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 }