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