001: /*
002: * Copyright 2005-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.module.financial.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.AmountTotaling;
025: import org.kuali.core.document.Copyable;
026: import org.kuali.core.rule.event.KualiDocumentEvent;
027: import org.kuali.core.service.BusinessObjectService;
028: import org.kuali.core.util.GlobalVariables;
029: import org.kuali.core.util.KualiDecimal;
030: import org.kuali.core.util.ObjectUtils;
031: import org.kuali.core.web.format.CurrencyFormatter;
032: import org.kuali.kfs.KFSConstants;
033: import org.kuali.kfs.context.SpringContext;
034: import org.kuali.module.financial.bo.CashReceiptHeader;
035: import org.kuali.module.financial.bo.Check;
036: import org.kuali.module.financial.bo.CheckBase;
037: import org.kuali.module.financial.bo.CoinDetail;
038: import org.kuali.module.financial.bo.CurrencyDetail;
039: import org.kuali.module.financial.rule.event.AddCheckEvent;
040: import org.kuali.module.financial.rule.event.DeleteCheckEvent;
041: import org.kuali.module.financial.rule.event.UpdateCheckEvent;
042: import org.kuali.module.financial.service.CashReceiptService;
043: import org.kuali.module.financial.service.CheckService;
044: import org.kuali.module.gl.util.SufficientFundsItem;
045:
046: /**
047: * This is the business object that represents the CashReceiptDocument in Kuali. This is a transactional document that will
048: * eventually post transactions to the G/L. It integrates with workflow. Since a Cash Receipt is a one sided transactional document,
049: * only accepting funds into the university, the accounting line data will be held in the source accounting line data structure
050: * only.
051: */
052: public class CashReceiptDocument extends CashReceiptFamilyBase
053: implements Copyable, AmountTotaling {
054: private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger
055: .getLogger(CashReceiptDocument.class);
056:
057: public static final String CHECK_ENTRY_DETAIL = "individual";
058: public static final String CHECK_ENTRY_TOTAL = "totals";
059:
060: public static final String DOCUMENT_TYPE = "CR";
061:
062: // child object containers - for all the different reconciliation detail sections
063: private String checkEntryMode = CHECK_ENTRY_DETAIL;
064: private List checks = new ArrayList();
065:
066: // incrementers for detail lines
067: private Integer nextCheckSequenceId = new Integer(1);
068:
069: // monetary attributes
070: private KualiDecimal totalCashAmount = KualiDecimal.ZERO;
071: private KualiDecimal totalCheckAmount = KualiDecimal.ZERO;
072: private KualiDecimal totalCoinAmount = KualiDecimal.ZERO;
073:
074: private CurrencyDetail currencyDetail;
075: private CoinDetail coinDetail;
076:
077: private CashReceiptHeader cashReceiptHeader;
078:
079: /**
080: * Initializes the array lists and line incrementers.
081: */
082: public CashReceiptDocument() {
083: super ();
084:
085: setCampusLocationCode(GlobalVariables.getUserSession()
086: .getUniversalUser().getCampusCode());
087: currencyDetail = new CurrencyDetail();
088: coinDetail = new CoinDetail();
089: }
090:
091: /**
092: * Gets the totalCashAmount attribute.
093: *
094: * @return Returns the totalCashAmount.
095: */
096: public KualiDecimal getTotalCashAmount() {
097: return (currencyDetail != null) ? currencyDetail
098: .getTotalAmount() : KualiDecimal.ZERO;
099: }
100:
101: /**
102: * This method returns the cash total amount as a currency formatted string.
103: *
104: * @return String
105: */
106: public String getCurrencyFormattedTotalCashAmount() {
107: return (String) new CurrencyFormatter()
108: .format(getTotalCashAmount());
109: }
110:
111: /**
112: * Sets the totalCashAmount attribute value.
113: *
114: * @param cashAmount The totalCashAmount to set.
115: */
116: public void setTotalCashAmount(KualiDecimal cashAmount) {
117: this .totalCashAmount = cashAmount;
118: }
119:
120: /**
121: * @param checkEntryMode
122: */
123: public void setCheckEntryMode(String checkEntryMode) {
124: this .checkEntryMode = checkEntryMode;
125: }
126:
127: /**
128: * @return checkEntryMode
129: */
130: public String getCheckEntryMode() {
131: return checkEntryMode;
132: }
133:
134: /**
135: * Gets the checks attribute.
136: *
137: * @return Returns the checks.
138: */
139: public List<Check> getChecks() {
140: return checks;
141: }
142:
143: /**
144: * Sets the checks attribute value.
145: *
146: * @param checks The checks to set.
147: */
148: public void setChecks(List checks) {
149: this .checks = checks;
150: }
151:
152: /**
153: * Gets the number of checks, since Sun doesn't have a direct getter for collection size
154: *
155: * @return the number of checks
156: */
157: public int getCheckCount() {
158: int count = 0;
159: if (ObjectUtils.isNotNull(checks)) {
160: count = checks.size();
161: }
162: return count;
163: }
164:
165: /**
166: * Adds a new check to the list.
167: *
168: * @param check
169: */
170: public void addCheck(Check check) {
171: check.setSequenceId(this .nextCheckSequenceId);
172:
173: this .checks.add(check);
174:
175: this .nextCheckSequenceId = new Integer(this .nextCheckSequenceId
176: .intValue() + 1);
177:
178: setTotalCheckAmount(getTotalCheckAmount()
179: .add(check.getAmount()));
180: }
181:
182: /**
183: * Retrieve a particular check at a given index in the list of checks.
184: *
185: * @param index
186: * @return Check
187: */
188: public Check getCheck(int index) {
189: while (this .checks.size() <= index) {
190: checks.add(createNewCheck());
191: }
192: return (Check) checks.get(index);
193: }
194:
195: /**
196: * @see org.kuali.kfs.document.AccountingDocumentBase#checkSufficientFunds()
197: */
198: @Override
199: public List<SufficientFundsItem> checkSufficientFunds() {
200: LOG.debug("checkSufficientFunds() started");
201:
202: // This document does not do sufficient funds checking
203: return new ArrayList<SufficientFundsItem>();
204: }
205:
206: /**
207: * Override to set the document status to VERIFIED ("V") when the document is FINAL. When the Cash Management document that this
208: * is associated with is FINAL approved, this status will be set to APPROVED ("A") to be picked up by the GL for processing.
209: * That's done in the handleRouteStatusChange() method in the CashManagementDocument.
210: *
211: * @see org.kuali.core.document.Document#handleRouteStatusChange()
212: */
213: @Override
214: public void handleRouteStatusChange() {
215: super .handleRouteStatusChange();
216: // Workflow Status of PROCESSED --> Kuali Doc Status of Verified
217: if (getDocumentHeader().getWorkflowDocument()
218: .stateIsProcessed()) {
219: this
220: .getDocumentHeader()
221: .setFinancialDocumentStatusCode(
222: KFSConstants.DocumentStatusCodes.CashReceipt.VERIFIED);
223: LOG.info("Adding Cash to Cash Drawer");
224: SpringContext.getBean(CashReceiptService.class)
225: .addCashDetailsToCashDrawer(this );
226: }
227: }
228:
229: /**
230: * This method removes a check from the list and updates the total appropriately.
231: *
232: * @param index
233: */
234: public void removeCheck(int index) {
235: Check check = (Check) checks.remove(index);
236: KualiDecimal newTotalCheckAmount = getTotalCheckAmount()
237: .subtract(check.getAmount());
238: // if the totalCheckAmount goes negative, bring back to zero.
239: if (newTotalCheckAmount.isNegative()) {
240: newTotalCheckAmount = KualiDecimal.ZERO;
241: }
242: setTotalCheckAmount(newTotalCheckAmount);
243: }
244:
245: /**
246: * Gets the nextCheckSequenceId attribute.
247: *
248: * @return Returns the nextCheckSequenceId.
249: */
250: public Integer getNextCheckSequenceId() {
251: return nextCheckSequenceId;
252: }
253:
254: /**
255: * Sets the nextCheckSequenceId attribute value.
256: *
257: * @param nextCheckSequenceId The nextCheckSequenceId to set.
258: */
259: public void setNextCheckSequenceId(Integer nextCheckSequenceId) {
260: this .nextCheckSequenceId = nextCheckSequenceId;
261: }
262:
263: /**
264: * Gets the totalCheckAmount attribute.
265: *
266: * @return Returns the totalCheckAmount.
267: */
268: public KualiDecimal getTotalCheckAmount() {
269: if (totalCheckAmount == null) {
270: setTotalCheckAmount(KualiDecimal.ZERO);
271: }
272: return totalCheckAmount;
273: }
274:
275: /**
276: * This method returns the check total amount as a currency formatted string.
277: *
278: * @return String
279: */
280: public String getCurrencyFormattedTotalCheckAmount() {
281: return (String) new CurrencyFormatter()
282: .format(getTotalCheckAmount());
283: }
284:
285: /**
286: * Sets the totalCheckAmount attribute value.
287: *
288: * @param totalCheckAmount The totalCheckAmount to set.
289: */
290: public void setTotalCheckAmount(KualiDecimal totalCheckAmount) {
291: this .totalCheckAmount = totalCheckAmount;
292: }
293:
294: /**
295: * Gets the totalCoinAmount attribute.
296: *
297: * @return Returns the totalCoinAmount.
298: */
299: public KualiDecimal getTotalCoinAmount() {
300: return (coinDetail != null) ? coinDetail.getTotalAmount()
301: : KualiDecimal.ZERO;
302: }
303:
304: /**
305: * This method returns the coin total amount as a currency formatted string.
306: *
307: * @return String
308: */
309: public String getCurrencyFormattedTotalCoinAmount() {
310: return (String) new CurrencyFormatter()
311: .format(getTotalCoinAmount());
312: }
313:
314: /**
315: * Sets the totalCoinAmount attribute value.
316: *
317: * @param totalCoinAmount The totalCoinAmount to set.
318: */
319: public void setTotalCoinAmount(KualiDecimal totalCoinAmount) {
320: this .totalCoinAmount = totalCoinAmount;
321: }
322:
323: /**
324: * This method returns the overall total of the document - coin plus check plus cash.
325: *
326: * @see org.kuali.kfs.document.AccountingDocumentBase#getTotalDollarAmount()
327: * @return KualiDecimal
328: */
329: @Override
330: public KualiDecimal getTotalDollarAmount() {
331: KualiDecimal sumTotalAmount = getTotalCoinAmount().add(
332: getTotalCheckAmount()).add(getTotalCashAmount());
333: return sumTotalAmount;
334: }
335:
336: /**
337: * Gets the coinDetail attribute.
338: *
339: * @return Returns the coinDetail.
340: */
341: public CoinDetail getCoinDetail() {
342: return coinDetail;
343: }
344:
345: /**
346: * Sets the coinDetail attribute value.
347: *
348: * @param coinDetail The coinDetail to set.
349: */
350: public void setCoinDetail(CoinDetail coinDetail) {
351: this .coinDetail = coinDetail;
352: }
353:
354: /**
355: * Gets the currencyDetail attribute.
356: *
357: * @return Returns the currencyDetail.
358: */
359: public CurrencyDetail getCurrencyDetail() {
360: return currencyDetail;
361: }
362:
363: /**
364: * Sets the currencyDetail attribute value.
365: *
366: * @param currencyDetail The currencyDetail to set.
367: */
368: public void setCurrencyDetail(CurrencyDetail currencyDetail) {
369: this .currencyDetail = currencyDetail;
370: }
371:
372: /**
373: * Retrieves the summed total amount in a currency format with commas.
374: *
375: * @return String
376: */
377: public String getCurrencyFormattedSumTotalAmount() {
378: return (String) new CurrencyFormatter()
379: .format(getTotalDollarAmount());
380: }
381:
382: /**
383: * @return sum of the amounts of the current list of checks
384: */
385: public KualiDecimal calculateCheckTotal() {
386: KualiDecimal total = KualiDecimal.ZERO;
387: for (Iterator i = getChecks().iterator(); i.hasNext();) {
388: Check c = (Check) i.next();
389: if (null != c.getAmount()) {
390: total = total.add(c.getAmount());
391: }
392: }
393: return total;
394: }
395:
396: /**
397: * @see org.kuali.core.document.DocumentBase#prepareForSave()
398: */
399: @Override
400: public void prepareForSave() {
401: super .prepareForSave();
402:
403: // clear check list if mode is checkTotal
404: if (CHECK_ENTRY_TOTAL.equals(getCheckEntryMode())) {
405: getChecks().clear();
406: }
407: // update total if mode is checkDetail
408: else {
409: setTotalCheckAmount(calculateCheckTotal());
410: }
411: }
412:
413: /**
414: * @see org.kuali.core.document.DocumentBase#processAfterRetrieve()
415: */
416: @Override
417: public void processAfterRetrieve() {
418: super .processAfterRetrieve();
419:
420: // set to checkTotal mode if no checks
421: List checkList = getChecks();
422: if (ObjectUtils.isNull(checkList) || checkList.isEmpty()) {
423: setCheckEntryMode(CHECK_ENTRY_TOTAL);
424: }
425: // set to checkDetail mode if checks (and update the checkTotal, while you're here)
426: else {
427: setCheckEntryMode(CHECK_ENTRY_DETAIL);
428: setTotalCheckAmount(calculateCheckTotal());
429: }
430: refreshCashDetails();
431: }
432:
433: /**
434: * @see org.kuali.core.document.DocumentBase#postProcessSave(org.kuali.core.rule.event.KualiDocumentEvent)
435: */
436: @Override
437: public void postProcessSave(KualiDocumentEvent event) {
438: super .postProcessSave(event);
439:
440: if (retrieveCurrencyDetail() == null) {
441: getCurrencyDetail().setDocumentNumber(
442: this .getDocumentNumber());
443: getCurrencyDetail().setFinancialDocumentTypeCode(
444: CashReceiptDocument.DOCUMENT_TYPE);
445: getCurrencyDetail().setCashieringRecordSource(
446: KFSConstants.CurrencyCoinSources.CASH_RECEIPTS);
447: }
448:
449: if (retrieveCoinDetail() == null) {
450: getCoinDetail().setDocumentNumber(this .getDocumentNumber());
451: getCoinDetail().setFinancialDocumentTypeCode(
452: CashReceiptDocument.DOCUMENT_TYPE);
453: getCoinDetail().setCashieringRecordSource(
454: KFSConstants.CurrencyCoinSources.CASH_RECEIPTS);
455: }
456:
457: SpringContext.getBean(BusinessObjectService.class).save(
458: getCurrencyDetail());
459: SpringContext.getBean(BusinessObjectService.class).save(
460: getCoinDetail());
461: }
462:
463: /**
464: * This method refreshes the currency/coin details for this cash receipt document
465: */
466: public void refreshCashDetails() {
467: this .currencyDetail = retrieveCurrencyDetail();
468: this .coinDetail = retrieveCoinDetail();
469: }
470:
471: /**
472: * Get this document's currency detail from the database
473: *
474: * @return the currency detail record for this cash receipt document
475: */
476: private CurrencyDetail retrieveCurrencyDetail() {
477: return (CurrencyDetail) SpringContext.getBean(
478: BusinessObjectService.class).findByPrimaryKey(
479: CurrencyDetail.class, getCashDetailPrimaryKey());
480: }
481:
482: /**
483: * Grab this document's coin detail from the database
484: *
485: * @return the coin detail record for this cash receipt document
486: */
487: private CoinDetail retrieveCoinDetail() {
488: return (CoinDetail) SpringContext.getBean(
489: BusinessObjectService.class).findByPrimaryKey(
490: CoinDetail.class, getCashDetailPrimaryKey());
491: }
492:
493: /**
494: * Gets the cashReceiptHeader attribute.
495: *
496: * @return Returns the cashReceiptHeader.
497: */
498: public CashReceiptHeader getCashReceiptHeader() {
499: return cashReceiptHeader;
500: }
501:
502: /**
503: * Sets the cashReceiptHeader attribute value.
504: *
505: * @param cashReceiptHeader The cashReceiptHeader to set.
506: */
507: public void setCashReceiptHeader(CashReceiptHeader cashReceiptHeader) {
508: this .cashReceiptHeader = cashReceiptHeader;
509: }
510:
511: /**
512: * Generate the primary key for a currency or coin detail related to this document
513: *
514: * @return a map with a representation of the proper primary key
515: */
516: private Map getCashDetailPrimaryKey() {
517: Map pk = new HashMap();
518: pk.put("documentNumber", this .getDocumentNumber());
519: pk.put("financialDocumentTypeCode",
520: CashReceiptDocument.DOCUMENT_TYPE);
521: pk.put("cashieringRecordSource",
522: KFSConstants.CurrencyCoinSources.CASH_RECEIPTS);
523: return pk;
524: }
525:
526: /**
527: * @see org.kuali.core.document.TransactionalDocumentBase#buildListOfDeletionAwareLists()
528: */
529: @Override
530: public List buildListOfDeletionAwareLists() {
531: List managedLists = super .buildListOfDeletionAwareLists();
532: managedLists.add(getChecks());
533:
534: return managedLists;
535: }
536:
537: @Override
538: public List generateSaveEvents() {
539: // 1. retrieve persisted checks for document
540: // 2. retrieve current checks from given document
541: // 3. compare, creating add/delete/update events as needed
542: // 4. apply rules as appropriate returned events
543: List persistedChecks = SpringContext
544: .getBean(CheckService.class).getByDocumentHeaderId(
545: getDocumentNumber());
546: List currentChecks = getChecks();
547:
548: List events = generateEvents(persistedChecks, currentChecks,
549: KFSConstants.EXISTING_CHECK_PROPERTY_NAME, this );
550:
551: return events;
552: }
553:
554: /**
555: * Generates a List of instances of CheckEvent subclasses, one for each changed check in the union of the persistedLines and
556: * currentLines lists. Events in the list will be grouped in order by event-type (update, add, delete).
557: *
558: * @param persistedChecks
559: * @param currentChecks
560: * @param errorPathPrefix
561: * @param crdoc
562: * @return List of CheckEvent subclass instances
563: */
564: private List generateEvents(List persistedChecks,
565: List currentChecks, String errorPathPrefix,
566: CashReceiptFamilyBase crdoc) {
567: List addEvents = new ArrayList();
568: List updateEvents = new ArrayList();
569: List deleteEvents = new ArrayList();
570:
571: //
572: // generate events
573: Map persistedCheckMap = buildCheckMap(persistedChecks);
574:
575: // (iterate through current lines to detect additions and updates, removing affected lines from persistedLineMap as we go
576: // so deletions can be detected by looking at whatever remains in persistedLineMap)
577: int index = 0;
578: for (Iterator i = currentChecks.iterator(); i.hasNext(); index++) {
579: Check currentCheck = (Check) i.next();
580: Integer key = currentCheck.getSequenceId();
581:
582: Check persistedCheck = (Check) persistedCheckMap.get(key);
583: // if line is both current and persisted...
584: if (persistedCheck != null) {
585: // ...check for updates
586: if (!currentCheck.isLike(persistedCheck)) {
587: UpdateCheckEvent updateEvent = new UpdateCheckEvent(
588: errorPathPrefix, crdoc, currentCheck);
589: updateEvents.add(updateEvent);
590: } else {
591: // do nothing, since this line hasn't changed
592: }
593:
594: persistedCheckMap.remove(key);
595: } else {
596: // it must be a new addition
597: AddCheckEvent addEvent = new AddCheckEvent(
598: errorPathPrefix, crdoc, currentCheck);
599: addEvents.add(addEvent);
600: }
601: }
602:
603: // detect deletions
604: for (Iterator i = persistedCheckMap.entrySet().iterator(); i
605: .hasNext();) {
606: Map.Entry e = (Map.Entry) i.next();
607: Check persistedCheck = (Check) e.getValue();
608: DeleteCheckEvent deleteEvent = new DeleteCheckEvent(
609: errorPathPrefix, crdoc, persistedCheck);
610: deleteEvents.add(deleteEvent);
611: }
612:
613: //
614: // merge the lists
615: List lineEvents = new ArrayList();
616: lineEvents.addAll(updateEvents);
617: lineEvents.addAll(addEvents);
618: lineEvents.addAll(deleteEvents);
619:
620: return lineEvents;
621: }
622:
623: /**
624: * @param checks
625: * @return Map containing Checks from the given List, indexed by their sequenceId
626: */
627: private Map buildCheckMap(List checks) {
628: Map checkMap = new HashMap();
629:
630: for (Iterator i = checks.iterator(); i.hasNext();) {
631: Check check = (Check) i.next();
632: Integer sequenceId = check.getSequenceId();
633:
634: Object oldCheck = checkMap.put(sequenceId, check);
635:
636: // verify that sequence numbers are unique...
637: if (oldCheck != null) {
638: throw new IllegalStateException(
639: "sequence id collision detected for sequence id "
640: + sequenceId);
641: }
642: }
643:
644: return checkMap;
645: }
646:
647: public Check createNewCheck() {
648: Check newCheck = new CheckBase();
649: newCheck.setFinancialDocumentTypeCode(DOCUMENT_TYPE);
650: newCheck
651: .setCashieringRecordSource(KFSConstants.CheckSources.CASH_RECEIPTS);
652: return newCheck;
653: }
654:
655: }
|