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