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 017 package org.kuali.kfs.fp.document; 018 019 import static org.kuali.rice.kns.util.AssertionUtils.assertThat; 020 021 import java.util.ArrayList; 022 import java.util.HashMap; 023 import java.util.Iterator; 024 import java.util.LinkedHashMap; 025 import java.util.List; 026 import java.util.Map; 027 028 import org.apache.commons.lang.StringUtils; 029 import org.apache.log4j.Logger; 030 import org.kuali.kfs.fp.businessobject.CashDrawer; 031 import org.kuali.kfs.fp.businessobject.CashieringItemInProcess; 032 import org.kuali.kfs.fp.businessobject.CashieringTransaction; 033 import org.kuali.kfs.fp.businessobject.Check; 034 import org.kuali.kfs.fp.businessobject.Deposit; 035 import org.kuali.kfs.fp.document.service.CashManagementService; 036 import org.kuali.kfs.fp.service.CashDrawerService; 037 import org.kuali.kfs.sys.KFSConstants; 038 import org.kuali.kfs.sys.KFSKeyConstants; 039 import org.kuali.kfs.sys.KFSPropertyConstants; 040 import org.kuali.kfs.sys.KFSConstants.DepositConstants; 041 import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry; 042 import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper; 043 import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail; 044 import org.kuali.kfs.sys.context.SpringContext; 045 import org.kuali.kfs.sys.document.GeneralLedgerPendingEntrySource; 046 import org.kuali.kfs.sys.document.GeneralLedgerPostingDocumentBase; 047 import org.kuali.kfs.sys.document.service.AccountingDocumentRuleHelperService; 048 import org.kuali.kfs.sys.service.BankService; 049 import org.kuali.kfs.sys.service.GeneralLedgerPendingEntryService; 050 import org.kuali.kfs.sys.service.UniversityDateService; 051 import org.kuali.rice.kew.dto.DocumentRouteStatusChangeDTO; 052 import org.kuali.rice.kns.bo.Campus; 053 import org.kuali.rice.kns.bo.CampusImpl; 054 import org.kuali.rice.kns.exception.ValidationException; 055 import org.kuali.rice.kns.rule.event.KualiDocumentEvent; 056 import org.kuali.rice.kns.service.BusinessObjectService; 057 import org.kuali.rice.kns.service.DateTimeService; 058 import org.kuali.rice.kns.service.KualiModuleService; 059 import org.kuali.rice.kns.util.KNSPropertyConstants; 060 import org.kuali.rice.kns.util.KualiDecimal; 061 import org.kuali.rice.kns.util.ObjectUtils; 062 import org.kuali.rice.kns.workflow.service.KualiWorkflowDocument; 063 064 /** 065 * This class represents the CashManagementDocument. 066 */ 067 public class CashManagementDocument extends GeneralLedgerPostingDocumentBase implements GeneralLedgerPendingEntrySource { 068 protected static final long serialVersionUID = 7475843770851900297L; 069 protected static Logger LOG = Logger.getLogger(CashManagementDocument.class); 070 071 protected String campusCode; 072 protected String referenceFinancialDocumentNumber; 073 074 protected List<Deposit> deposits; 075 076 protected List<Check> checks; 077 078 protected CashieringTransaction currentTransaction; 079 protected CashDrawer cashDrawer; 080 protected Campus campus; 081 082 /** 083 * Default constructor. 084 */ 085 public CashManagementDocument() { 086 super(); 087 deposits = new ArrayList<Deposit>(); 088 checks = new ArrayList<Check>(); 089 this.resetCurrentTransaction(); 090 } 091 092 093 /** 094 * @return current value of referenceFinancialDocumentNumber. 095 */ 096 public String getReferenceFinancialDocumentNumber() { 097 return referenceFinancialDocumentNumber; 098 } 099 100 /** 101 * Sets the referenceFinancialDocumentNumber attribute value. 102 * 103 * @param referenceFinancialDocumentNumber The referenceFinancialDocumentNumber to set. 104 */ 105 public void setReferenceFinancialDocumentNumber(String referenceFinancialDocumentNumber) { 106 this.referenceFinancialDocumentNumber = referenceFinancialDocumentNumber; 107 } 108 109 110 /** 111 * @return current value of campusCode. 112 */ 113 public String getCampusCode() { 114 return campusCode; 115 } 116 117 /** 118 * Sets the campusCode attribute value. 119 * 120 * @param campusCode The campusCode to set. 121 */ 122 public void setCampusCode(String campusCode) { 123 this.campusCode = campusCode; 124 } 125 126 /** 127 * Derives and returns the cash drawer status for the document's workgroup 128 */ 129 public String getCashDrawerStatus() { 130 return getCashDrawer().getStatusCode(); 131 } 132 133 /** 134 * @param cashDrawerStatus 135 */ 136 public void setCashDrawerStatus(String cashDrawerStatus) { 137 // ignored, because that value is dynamically retrieved from the service 138 // required, because POJO pitches a fit if this method doesn't exist 139 } 140 141 /** 142 * Alias for getCashDrawerStatus which avoids the automagic formatting 143 */ 144 public String getRawCashDrawerStatus() { 145 return getCashDrawerStatus(); 146 } 147 148 /* Deposit-list maintenance */ 149 /** 150 * @return current List of Deposits 151 */ 152 public List<Deposit> getDeposits() { 153 return deposits; 154 } 155 156 /** 157 * Sets the current List of Deposits 158 * 159 * @param deposits 160 */ 161 public void setDeposits(List<Deposit> deposits) { 162 this.deposits = deposits; 163 } 164 165 /** 166 * Implementation creates empty Deposits as a side-effect, so that Struts' efforts to set fields of lines which haven't been 167 * created will succeed rather than causing a NullPointerException. 168 * 169 * @return Deposit at the given index 170 */ 171 public Deposit getDeposit(int index) { 172 extendDeposits(index + 1); 173 174 return (Deposit) deposits.get(index); 175 } 176 177 /** 178 * Removes and returns the Deposit at the given index. 179 * 180 * @param index 181 * @return Deposit at the given index 182 */ 183 public Deposit removeDeposit(int index) { 184 extendDeposits(index + 1); 185 186 return (Deposit) deposits.remove(index); 187 } 188 189 190 /** 191 * @return true if one of the Deposits contained in this document has a type of "final" 192 */ 193 public boolean hasFinalDeposit() { 194 boolean hasFinal = false; 195 196 for (Iterator i = deposits.iterator(); !hasFinal && i.hasNext();) { 197 Deposit d = (Deposit) i.next(); 198 199 hasFinal = StringUtils.equals(DepositConstants.DEPOSIT_TYPE_FINAL, d.getDepositTypeCode()); 200 } 201 202 return hasFinal; 203 } 204 205 /** 206 * @return lowest unused deposit-line-number, to simplify adding and canceling deposits out-of-order 207 */ 208 public Integer getNextDepositLineNumber() { 209 int maxLineNumber = -1; 210 211 for (Iterator i = deposits.iterator(); i.hasNext();) { 212 Deposit d = (Deposit) i.next(); 213 214 Integer depositLineNumber = d.getFinancialDocumentDepositLineNumber(); 215 if ((depositLineNumber != null) && (depositLineNumber.intValue() > maxLineNumber)) { 216 maxLineNumber = depositLineNumber.intValue(); 217 } 218 } 219 220 return new Integer(maxLineNumber + 1); 221 } 222 223 /** 224 * Adds default AccountingLineDecorators to sourceAccountingLineDecorators until it contains at least minSize elements 225 * 226 * @param minSize 227 */ 228 protected void extendDeposits(int minSize) { 229 while (deposits.size() < minSize) { 230 deposits.add(new Deposit()); 231 } 232 } 233 234 /** 235 * @see org.kuali.rice.kns.document.DocumentBase#buildListOfDeletionAwareLists() 236 */ 237 @Override 238 public List buildListOfDeletionAwareLists() { 239 List managedLists = super.buildListOfDeletionAwareLists(); 240 241 managedLists.add(getDeposits()); 242 243 return managedLists; 244 } 245 246 247 /** 248 * Gets the cashDrawer attribute. 249 * 250 * @return Returns the cashDrawer. 251 */ 252 public CashDrawer getCashDrawer() { 253 return cashDrawer; 254 } 255 256 /** 257 * Sets the cashDrawer attribute 258 * 259 * @param cd the cash drawer to set 260 */ 261 public void setCashDrawer(CashDrawer cd) { 262 cashDrawer = cd; 263 } 264 265 /** 266 * Gets the currentTransaction attribute. 267 * 268 * @return Returns the currentTransaction. 269 */ 270 public CashieringTransaction getCurrentTransaction() { 271 return currentTransaction; 272 } 273 274 275 /** 276 * Sets the currentTransaction attribute value. 277 * 278 * @param currentTransaction The currentTransaction to set. 279 */ 280 public void setCurrentTransaction(CashieringTransaction currentTransaction) { 281 this.currentTransaction = currentTransaction; 282 } 283 284 /** 285 * Gets the checks attribute. 286 * 287 * @return Returns the checks. 288 */ 289 public List<Check> getChecks() { 290 return checks; 291 } 292 293 /** 294 * Sets the checks attribute value. 295 * 296 * @param checks The checks to set. 297 */ 298 public void setChecks(List<Check> checks) { 299 this.checks = checks; 300 } 301 302 /** 303 * Add a check to the cash management document 304 * 305 * @param check 306 */ 307 public void addCheck(Check check) { 308 this.checks.add(check); 309 } 310 311 /** 312 * @see org.kuali.rice.kns.document.DocumentBase#doRouteStatusChange() 313 */ 314 @Override 315 public void doRouteStatusChange(DocumentRouteStatusChangeDTO statusChangeEvent) { 316 super.doRouteStatusChange(statusChangeEvent); 317 318 KualiWorkflowDocument kwd = getDocumentHeader().getWorkflowDocument(); 319 320 if (LOG.isDebugEnabled()) { 321 logState(); 322 } 323 324 if (kwd.stateIsProcessed()) { 325 // all approvals have been processed, finalize everything 326 SpringContext.getBean(CashManagementService.class).finalizeCashManagementDocument(this); 327 } 328 else if (kwd.stateIsCanceled() || kwd.stateIsDisapproved()) { 329 // document has been canceled or disapproved 330 SpringContext.getBean(CashManagementService.class).cancelCashManagementDocument(this); 331 } 332 } 333 334 protected void logState() { 335 KualiWorkflowDocument kwd = getDocumentHeader().getWorkflowDocument(); 336 337 if (kwd.stateIsInitiated()) { 338 LOG.debug("CMD stateIsInitiated"); 339 } 340 if (kwd.stateIsProcessed()) { 341 LOG.debug("CMD stateIsProcessed"); 342 } 343 if (kwd.stateIsCanceled()) { 344 LOG.debug("CMD stateIsCanceled"); 345 } 346 if (kwd.stateIsDisapproved()) { 347 LOG.debug("CMD stateIsDisapproved"); 348 } 349 } 350 351 /** 352 * @see org.kuali.rice.kns.document.DocumentBase#processAfterRetrieve() 353 */ 354 @Override 355 public void processAfterRetrieve() { 356 super.processAfterRetrieve(); 357 // grab the cash drawer 358 if (this.getCampusCode() != null) { 359 this.cashDrawer = SpringContext.getBean(CashDrawerService.class).getByCampusCode(this.getCampusCode()); 360 this.resetCurrentTransaction(); 361 } 362 SpringContext.getBean(CashManagementService.class).populateCashDetailsForDeposit(this); 363 } 364 365 366 /* utility methods */ 367 /** 368 * @see org.kuali.rice.kns.bo.BusinessObjectBase#toStringMapper() 369 */ 370 @Override 371 protected LinkedHashMap toStringMapper() { 372 LinkedHashMap m = new LinkedHashMap(); 373 m.put(KFSPropertyConstants.DOCUMENT_NUMBER, getDocumentNumber()); 374 m.put("campusCode", getCampusCode()); 375 return m; 376 } 377 378 /** 379 * This method creates a clean current transaction to be the new current transaction on this document 380 */ 381 public void resetCurrentTransaction() { 382 if (this.currentTransaction != null) { 383 this.currentTransaction.setTransactionEnded(SpringContext.getBean(DateTimeService.class).getCurrentDate()); 384 } 385 currentTransaction = new CashieringTransaction(campusCode, referenceFinancialDocumentNumber); 386 if (this.getCampusCode() != null) { 387 List<CashieringItemInProcess> openItemsInProcess = SpringContext.getBean(CashManagementService.class).getOpenItemsInProcess(this); 388 if (openItemsInProcess != null) { 389 currentTransaction.setOpenItemsInProcess(openItemsInProcess); 390 } 391 currentTransaction.setNextCheckSequenceId(SpringContext.getBean(CashManagementService.class).selectNextAvailableCheckLineNumber(this.documentNumber)); 392 } 393 } 394 395 396 /** 397 * Does nothing, as there aren't any accounting lines on this doc, so no GeneralLedgerPendingEntrySourceDetail create GLPEs 398 * @see org.kuali.kfs.document.GeneralLedgerPostingHelper#customizeExplicitGeneralLedgerPendingEntry(org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail, org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry) 399 */ 400 public void customizeExplicitGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySourceDetail postable, GeneralLedgerPendingEntry explicitEntry) {} 401 402 403 /** 404 * Does nothing save return true, as this document has no GLPEs created from a source of GeneralLedgerPostables 405 * @see org.kuali.kfs.document.GeneralLedgerPostingHelper#customizeOffsetGeneralLedgerPendingEntry(org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail, org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry, org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry) 406 */ 407 public boolean customizeOffsetGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySourceDetail accountingLine, GeneralLedgerPendingEntry explicitEntry, GeneralLedgerPendingEntry offsetEntry) { 408 return true; 409 } 410 411 412 /** 413 * Returns an empty list as this document has no GeneralLedgerPostables 414 * @see org.kuali.kfs.document.GeneralLedgerPostingHelper#getGeneralLedgerPostables() 415 */ 416 public List<GeneralLedgerPendingEntrySourceDetail> getGeneralLedgerPendingEntrySourceDetails() { 417 return new ArrayList<GeneralLedgerPendingEntrySourceDetail>(); 418 } 419 420 421 /** 422 * Always returns true, as there are no GeneralLedgerPostables to create GLPEs 423 * @see org.kuali.kfs.document.GeneralLedgerPostingHelper#isDebit(org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail) 424 */ 425 public boolean isDebit(GeneralLedgerPendingEntrySourceDetail postable) { 426 return true; 427 } 428 429 430 /** 431 * Generates bank offset GLPEs for deposits, if enabled. 432 * 433 * @param financialDocument submitted accounting document 434 * @param sequenceHelper helper class to keep track of sequence of general ledger pending entries 435 * @see org.kuali.kfs.document.GeneralLedgerPostingHelper#processGenerateDocumentGeneralLedgerPendingEntries(org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper) 436 */ 437 public boolean generateDocumentGeneralLedgerPendingEntries(GeneralLedgerPendingEntrySequenceHelper sequenceHelper) { 438 boolean success = true; 439 440 GeneralLedgerPendingEntryService glpeService = SpringContext.getBean(GeneralLedgerPendingEntryService.class); 441 442 if (SpringContext.getBean(BankService.class).isBankSpecificationEnabled()) { 443 Integer universityFiscalYear = getUniversityFiscalYear(); 444 int interimDepositNumber = 1; 445 for (Deposit deposit: getDeposits()) { 446 deposit.refreshReferenceObject(KFSPropertyConstants.BANK); 447 448 GeneralLedgerPendingEntry bankOffsetEntry = new GeneralLedgerPendingEntry(); 449 if (!glpeService.populateBankOffsetGeneralLedgerPendingEntry(deposit.getBank(), deposit.getDepositAmount(), this, universityFiscalYear, sequenceHelper, bankOffsetEntry, KFSConstants.CASH_MANAGEMENT_DEPOSIT_ERRORS)) { 450 success = false; 451 LOG.warn("Skipping ledger entries for deposit " + deposit.getDepositTicketNumber() + "."); 452 continue; // An unsuccessfully populated bank offset entry may contain invalid relations, so don't add it 453 } 454 455 bankOffsetEntry.setTransactionLedgerEntryDescription(createDescription(deposit, interimDepositNumber++)); 456 getGeneralLedgerPendingEntries().add(bankOffsetEntry); 457 sequenceHelper.increment(); 458 459 GeneralLedgerPendingEntry offsetEntry = (GeneralLedgerPendingEntry) ObjectUtils.deepCopy(bankOffsetEntry); 460 success &= glpeService.populateOffsetGeneralLedgerPendingEntry(universityFiscalYear, bankOffsetEntry, sequenceHelper, offsetEntry); 461 getGeneralLedgerPendingEntries().add(offsetEntry); 462 sequenceHelper.increment(); 463 } 464 } 465 466 return success; 467 } 468 469 /** 470 * Create description for deposit 471 * 472 * @param deposit deposit from cash management document 473 * @param interimDepositNumber 474 * @return the description for the given deposit's GLPE bank offset 475 */ 476 protected static String createDescription(Deposit deposit, int interimDepositNumber) { 477 String descriptionKey; 478 if (KFSConstants.DepositConstants.DEPOSIT_TYPE_FINAL.equals(deposit.getDepositTypeCode())) { 479 descriptionKey = KFSKeyConstants.CashManagement.DESCRIPTION_GLPE_BANK_OFFSET_FINAL; 480 } 481 else { 482 assertThat(KFSConstants.DepositConstants.DEPOSIT_TYPE_INTERIM.equals(deposit.getDepositTypeCode()), deposit.getDepositTypeCode()); 483 descriptionKey = KFSKeyConstants.CashManagement.DESCRIPTION_GLPE_BANK_OFFSET_INTERIM; 484 } 485 AccountingDocumentRuleHelperService accountingDocumentRuleUtil = SpringContext.getBean(AccountingDocumentRuleHelperService.class); 486 return accountingDocumentRuleUtil.formatProperty(descriptionKey, interimDepositNumber); 487 } 488 489 /** 490 * Gets the fiscal year for the GLPEs generated by this document. This works the same way as in TransactionalDocumentBase. The 491 * property is down in TransactionalDocument because no FinancialDocument (currently only CashManagementDocument) allows the 492 * user to override it. So, that logic is duplicated here. A comment in TransactionalDocumentBase says that this implementation 493 * is a hack right now because it's intended to be set by the 494 * <code>{@link org.kuali.kfs.coa.service.AccountingPeriodService}</code>, which suggests to me that pulling that 495 * property up to FinancialDocument is preferable to duplicating this logic here. 496 * 497 * @return the fiscal year for the GLPEs generated by this document 498 */ 499 protected Integer getUniversityFiscalYear() { 500 return SpringContext.getBean(UniversityDateService.class).getCurrentFiscalYear(); 501 } 502 503 /** 504 * The Cash Management doc doesn't have accounting lines, so it doesn't create general ledger pending entries for the accounting lines it doesn't have 505 * @see org.kuali.kfs.sys.document.GeneralLedgerPendingEntrySource#generateGeneralLedgerPendingEntries(org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail, org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper) 506 */ 507 public boolean generateGeneralLedgerPendingEntries(GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, GeneralLedgerPendingEntrySequenceHelper sequenceHelper) { 508 return true; 509 } 510 511 512 /** 513 * @see org.kuali.kfs.sys.document.GeneralLedgerPendingEntrySource#getGeneralLedgerPendingEntryAmountForGeneralLedgerPostable(org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail) 514 */ 515 public KualiDecimal getGeneralLedgerPendingEntryAmountForDetail(GeneralLedgerPendingEntrySourceDetail postable) { 516 return postable.getAmount().abs(); 517 } 518 519 /** 520 * Helper method on document for determining whether the document can have GLPEs. 521 * 522 * @return true if document can have GLPEs 523 */ 524 public boolean getBankCashOffsetEnabled() { 525 return SpringContext.getBean(BankService.class).isBankSpecificationEnabled(); 526 } 527 528 /** 529 * @see org.kuali.kfs.sys.document.GeneralLedgerPostingDocumentBase#prepareForSave(org.kuali.rice.kns.rule.event.KualiDocumentEvent) 530 */ 531 @Override 532 public void prepareForSave(KualiDocumentEvent event) { 533 if (getBankCashOffsetEnabled()) { 534 if (!SpringContext.getBean(GeneralLedgerPendingEntryService.class).generateGeneralLedgerPendingEntries(this)) { 535 logErrors(); 536 throw new ValidationException("general ledger GLPE generation failed"); 537 } 538 } 539 540 super.prepareForSave(event); 541 } 542 543 /** 544 * @return the campus associated with this cash drawer 545 */ 546 public Campus getCampus() { 547 if (campusCode != null && (campus == null || !campus.getCampusCode().equals(campusCode))) { 548 campus = retrieveCampus(); 549 } 550 return campus; 551 } 552 553 protected Campus retrieveCampus() { 554 Map<String, Object> criteria = new HashMap<String, Object>(); 555 criteria.put(KNSPropertyConstants.CAMPUS_CODE, campusCode); 556 return campus = (Campus) SpringContext.getBean(KualiModuleService.class).getResponsibleModuleService(Campus.class).getExternalizableBusinessObject(Campus.class, criteria); 557 } 558 }