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