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.module.ld.document.validation.impl;
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.apache.commons.lang.StringUtils;
025    import org.kuali.kfs.coa.businessobject.Account;
026    import org.kuali.kfs.module.ld.LaborConstants ;
027    import org.kuali.kfs.module.ld.LaborKeyConstants; 
028    import org.kuali.kfs.module.ld.LaborPropertyConstants;
029    import org.kuali.kfs.module.ld.businessobject.ExpenseTransferAccountingLine;
030    import org.kuali.kfs.module.ld.businessobject.ExpenseTransferSourceAccountingLine;
031    import org.kuali.kfs.module.ld.businessobject.LaborObject;
032    import org.kuali.kfs.module.ld.document.LaborExpenseTransferDocumentBase;
033    import org.kuali.kfs.module.ld.document.SalaryExpenseTransferDocument;
034    import org.kuali.kfs.sys.KFSKeyConstants;
035    import org.kuali.kfs.sys.KFSPropertyConstants;
036    import org.kuali.kfs.sys.ObjectUtil;
037    import org.kuali.kfs.sys.businessobject.AccountingLine;
038    import org.kuali.kfs.sys.document.AccountingDocument;
039    import org.kuali.kfs.sys.document.validation.GenericValidation;
040    import org.kuali.kfs.sys.document.validation.event.AttributedDocumentEvent;
041    import org.kuali.rice.kns.document.Document;
042    import org.kuali.rice.kns.util.GlobalVariables;
043    import org.kuali.rice.kns.util.KualiDecimal;
044    
045    /**
046     * check to ensure totals of accounting lines in source and target sections match by pay FY + pay period
047     * 
048     * @param accountingDocument the given document
049     * @return true if the given accounting lines in source and target match by pay fy and pp
050     */
051    public class LaborExpenseTransferAccountingLineTotalsMatchByPayFYAndPayPeriodValidation extends GenericValidation {
052        private Document documentForValidation;
053        
054        /**
055         * Validates before the document routes 
056         * @see org.kuali.kfs.validation.Validation#validate(java.lang.Object[])
057         */
058        public boolean validate(AttributedDocumentEvent event) {
059            boolean result = true;
060                 
061            Document documentForValidation = getDocumentForValidation();
062            
063            LaborExpenseTransferDocumentBase expenseTransferDocument = (LaborExpenseTransferDocumentBase) documentForValidation;
064            
065            List sourceLines = expenseTransferDocument.getSourceAccountingLines();
066            List targetLines = expenseTransferDocument.getTargetAccountingLines();
067    
068            // check to ensure totals of accounting lines in source and target sections match
069            if (!isAccountingLineTotalsMatchByPayFYAndPayPeriod(sourceLines, targetLines)) {
070                GlobalVariables.getMessageMap().putError(KFSPropertyConstants.SOURCE_ACCOUNTING_LINES, LaborKeyConstants.ACCOUNTING_LINE_TOTALS_BY_PAYFY_PAYPERIOD_MISMATCH_ERROR);
071                return false;
072            }
073    
074            return result;       
075        }
076    
077        /**
078         * This method calls other methods to check if all source and target accounting lines match between each set by pay fiscal year
079         * and pay period, returning true if the totals match, false otherwise.
080         * 
081         * @param sourceLines
082         * @param targetLines
083         * @return
084         */
085        public boolean isAccountingLineTotalsMatchByPayFYAndPayPeriod(List sourceLines, List targetLines) {
086            boolean isValid = true;
087    
088            // sum source lines by pay fy and pay period, store in map by key PayFY+PayPeriod
089            Map sourceLinesMap = sumAccountingLineAmountsByPayFYAndPayPeriod(sourceLines);
090    
091            // sum source lines by pay fy and pay period, store in map by key PayFY+PayPeriod
092            Map targetLinesMap = sumAccountingLineAmountsByPayFYAndPayPeriod(targetLines);
093    
094            // if totals don't match by PayFY+PayPeriod categories, then add error message
095            if (compareAccountingLineTotalsByPayFYAndPayPeriod(sourceLinesMap, targetLinesMap) == false) {
096                isValid = false;
097            }
098    
099            return isValid;
100        }
101          
102        /**
103         * This method sums the totals of each accounting line, making an entry in a map for each unique pay fiscal year and pay period.
104         * 
105         * @param accountingLines
106         * @return
107         */
108        protected Map sumAccountingLineAmountsByPayFYAndPayPeriod(List accountingLines) {
109    
110            ExpenseTransferAccountingLine line = null;
111            KualiDecimal linesAmount = KualiDecimal.ZERO;
112            Map linesMap = new HashMap();
113            String payFYPeriodKey = null;
114    
115            // go through source lines adding amounts to appropriate place in map
116            for (Iterator i = accountingLines.iterator(); i.hasNext();) {
117                // initialize
118                line = (ExpenseTransferAccountingLine) i.next();
119                linesAmount = KualiDecimal.ZERO;
120    
121                // create hash key
122                payFYPeriodKey = createPayFYPeriodKey(line.getPayrollEndDateFiscalYear(), line.getPayrollEndDateFiscalPeriodCode());
123    
124                // if entry exists, pull from hash
125                if (linesMap.containsKey(payFYPeriodKey)) {
126                    linesAmount = (KualiDecimal) linesMap.get(payFYPeriodKey);
127                }
128    
129                // update and store
130                linesAmount = linesAmount.add(line.getAmount());
131                linesMap.put(payFYPeriodKey, linesAmount);
132            }
133    
134            return linesMap;
135        }
136        
137        /**
138         * This method returns a String that is a concatenation of pay fiscal year and pay period code.
139         * 
140         * @param payFiscalYear
141         * @param payPeriodCode
142         * @return
143         */
144        protected String createPayFYPeriodKey(Integer payFiscalYear, String payPeriodCode) {
145    
146            StringBuffer payFYPeriodKey = new StringBuffer();
147    
148            payFYPeriodKey.append(payFiscalYear);
149            payFYPeriodKey.append(payPeriodCode);
150    
151            return payFYPeriodKey.toString();
152        }
153    
154        
155        /**
156         * This method checks that the total amount of labor ledger accounting lines in the document's FROM section is equal to the
157         * total amount on the labor ledger accounting lines TO section for each unique combination of pay fiscal year and pay period. A
158         * value of true is returned if all amounts for each unique combination between source and target accounting lines match, false
159         * otherwise.
160         * 
161         * @param sourceLinesMap
162         * @param targetLinesMap
163         * @return
164         */
165        protected boolean compareAccountingLineTotalsByPayFYAndPayPeriod(Map sourceLinesMap, Map targetLinesMap) {
166    
167            boolean isValid = true;
168            Map.Entry entry = null;
169            String currentKey = null;
170            KualiDecimal sourceLinesAmount = KualiDecimal.ZERO;
171            KualiDecimal targetLinesAmount = KualiDecimal.ZERO;
172    
173    
174            // Loop through source lines comparing against target lines
175            for (Iterator i = sourceLinesMap.entrySet().iterator(); i.hasNext() && isValid;) {
176                // initialize
177                entry = (Map.Entry) i.next();
178                currentKey = (String) entry.getKey();
179                sourceLinesAmount = (KualiDecimal) entry.getValue();
180    
181                if (targetLinesMap.containsKey(currentKey)) {
182                    targetLinesAmount = (KualiDecimal) targetLinesMap.get(currentKey);
183    
184                    // return false if the matching key values do not total each other
185                    if (sourceLinesAmount.compareTo(targetLinesAmount) != 0) {
186                        isValid = false;
187                    }
188    
189                }
190                else {
191                    isValid = false;
192                }
193            }
194    
195            /*
196             * Now loop through target lines comparing against source lines. This finds missing entries from either direction (source or
197             * target)
198             */
199            for (Iterator i = targetLinesMap.entrySet().iterator(); i.hasNext() && isValid;) {
200                // initialize
201                entry = (Map.Entry) i.next();
202                currentKey = (String) entry.getKey();
203                targetLinesAmount = (KualiDecimal) entry.getValue();
204    
205                if (sourceLinesMap.containsKey(currentKey)) {
206                    sourceLinesAmount = (KualiDecimal) sourceLinesMap.get(currentKey);
207    
208                    // return false if the matching key values do not total each other
209                    if (targetLinesAmount.compareTo(sourceLinesAmount) != 0) {
210                        isValid = false;
211                    }
212    
213                }
214                else {
215                    isValid = false;
216                }
217            }
218            return isValid;
219        }
220    
221        /**
222         * Gets the documentForValidation attribute. 
223         * @return Returns the documentForValidation.
224         */
225        public Document getDocumentForValidation() {
226            return documentForValidation;
227        }
228    
229        /**
230         * Sets the documentForValidation attribute value.
231         * @param documentForValidation The documentForValidation to set.
232         */
233        public void setDocumentForValidation(Document documentForValidation) {
234            this.documentForValidation = documentForValidation;
235        }    
236    }