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    }