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.validation.impl;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.kfs.fp.businessobject.DisbursementVoucherNonEmployeeTravel;
020    import org.kuali.kfs.fp.document.DisbursementVoucherConstants;
021    import org.kuali.kfs.fp.document.DisbursementVoucherDocument;
022    import org.kuali.kfs.fp.document.service.DisbursementVoucherTaxService;
023    import org.kuali.kfs.fp.document.service.DisbursementVoucherTravelService;
024    import org.kuali.kfs.sys.KFSConstants;
025    import org.kuali.kfs.sys.KFSKeyConstants;
026    import org.kuali.kfs.sys.KFSPropertyConstants;
027    import org.kuali.kfs.sys.document.AccountingDocument;
028    import org.kuali.kfs.sys.document.validation.GenericValidation;
029    import org.kuali.kfs.sys.document.validation.event.AttributedDocumentEvent;
030    import org.kuali.rice.kns.service.DictionaryValidationService;
031    import org.kuali.rice.kns.service.ParameterEvaluator;
032    import org.kuali.rice.kns.service.ParameterService;
033    import org.kuali.rice.kns.util.GlobalVariables;
034    import org.kuali.rice.kns.util.KualiDecimal;
035    import org.kuali.rice.kns.util.MessageMap;
036    import org.kuali.rice.kns.util.ObjectUtils;
037    
038    public class DisbursementVoucherNonEmployeeTravelValidation extends GenericValidation {
039        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DisbursementVoucherNonEmployeeTravelValidation.class);
040    
041        private ParameterService parameterService;
042        private DisbursementVoucherTaxService disbursementVoucherTaxService;
043        private DisbursementVoucherTravelService disbursementVoucherTravelService;
044        private DictionaryValidationService dictionaryValidationService;
045        private AccountingDocument accountingDocumentForValidation;
046        
047        /**
048         * @see org.kuali.kfs.sys.document.validation.Validation#validate(org.kuali.kfs.sys.document.validation.event.AttributedDocumentEvent)
049         */
050        public boolean validate(AttributedDocumentEvent event) {  
051            LOG.debug("validate start");
052            boolean isValid = true;
053            
054            DisbursementVoucherDocument document = (DisbursementVoucherDocument) accountingDocumentForValidation;
055            DisbursementVoucherNonEmployeeTravel nonEmployeeTravel = document.getDvNonEmployeeTravel();
056            
057            // skip the validation if the payment reason is not noneployee travel or the payee is an employee
058            if (!isTravelNonEmplPaymentReason(document) || document.getDvPayeeDetail().isEmployee()) {
059                return true;
060            }
061            
062            MessageMap errors = GlobalVariables.getMessageMap();
063            errors.addToErrorPath(KFSPropertyConstants.DOCUMENT);
064            errors.addToErrorPath(KFSPropertyConstants.DV_NON_EMPLOYEE_TRAVEL);
065            
066            getDictionaryValidationService().validateBusinessObjectsRecursively(document.getDvNonEmployeeTravel(), 1);
067    
068            /* travel from and to state required if country is us */
069            if (KFSConstants.COUNTRY_CODE_UNITED_STATES.equals(nonEmployeeTravel.getDvTravelFromCountryCode()) && StringUtils.isBlank(nonEmployeeTravel.getDisbVchrTravelFromStateCode())) {
070                errors.putError(KFSPropertyConstants.DISB_VCHR_TRAVEL_FROM_STATE_CODE, KFSKeyConstants.ERROR_DV_TRAVEL_FROM_STATE);
071                isValid = false;
072            }
073            
074            if (KFSConstants.COUNTRY_CODE_UNITED_STATES.equals(nonEmployeeTravel.getDisbVchrTravelToCountryCode()) && StringUtils.isBlank(nonEmployeeTravel.getDisbVchrTravelToStateCode())) {
075                errors.putError(KFSPropertyConstants.DISB_VCHR_TRAVEL_TO_STATE_CODE, KFSKeyConstants.ERROR_DV_TRAVEL_TO_STATE);
076                isValid = false;
077            }
078    
079            if (!isValid) {
080                errors.removeFromErrorPath(KFSPropertyConstants.DV_NON_EMPLOYEE_TRAVEL);
081                errors.removeFromErrorPath(KFSPropertyConstants.DOCUMENT);
082                return false;
083            }
084    
085            /* must fill in all required per diem fields if any field is filled in */
086            boolean perDiemSectionComplete = validatePerDiemSection(document, errors);
087    
088            /* must fill in all required personal vehicle fields if any field is filled in */
089            boolean personalVehicleSectionComplete = validatePersonalVehicleSection(document, errors);
090    
091            /* must have per diem change message if actual amount is different from calculated amount */
092            if (perDiemSectionComplete) { // Only validate if per diem section is filled in
093                if (nonEmployeeTravel.getDisbVchrPerdiemCalculatedAmt().compareTo(nonEmployeeTravel.getDisbVchrPerdiemActualAmount()) != 0 && StringUtils.isBlank(nonEmployeeTravel.getDvPerdiemChangeReasonText())) {
094                    errors.putError(KFSPropertyConstants.DV_PERDIEM_CHANGE_REASON_TEXT, KFSKeyConstants.ERROR_DV_PERDIEM_CHANGE_REQUIRED);
095                    isValid = false;
096                }
097            }
098    
099            /* make sure per diem fields have not changed since the per diem amount calculation */
100            if (perDiemSectionComplete) { // Only validate if per diem section is filled in
101                KualiDecimal calculatedPerDiem = getDisbursementVoucherTravelService().calculatePerDiemAmount(nonEmployeeTravel.getDvPerdiemStartDttmStamp(), nonEmployeeTravel.getDvPerdiemEndDttmStamp(), nonEmployeeTravel.getDisbVchrPerdiemRate());
102                if (calculatedPerDiem.compareTo(nonEmployeeTravel.getDisbVchrPerdiemCalculatedAmt()) != 0) {
103                    errors.putErrorWithoutFullErrorPath(KFSConstants.GENERAL_NONEMPLOYEE_TAB_ERRORS, KFSKeyConstants.ERROR_DV_PER_DIEM_CALC_CHANGE);
104                    isValid = false;
105                }
106            }
107    
108            // validate the tax amount
109            isValid &= validateTravelAmount(document);
110    
111            /* make sure mileage fields have not changed since the mileage amount calculation */
112            if (personalVehicleSectionComplete) {
113                KualiDecimal currentCalcAmt = document.getDvNonEmployeeTravel().getDisbVchrMileageCalculatedAmt();
114                KualiDecimal currentActualAmt = document.getDvNonEmployeeTravel().getDisbVchrPersonalCarAmount();
115                if (ObjectUtils.isNotNull(currentCalcAmt) && ObjectUtils.isNotNull(currentActualAmt)) {
116                    KualiDecimal calculatedMileageAmount = getDisbursementVoucherTravelService().calculateMileageAmount(document.getDvNonEmployeeTravel().getDvPersonalCarMileageAmount(), document.getDvNonEmployeeTravel().getDvPerdiemStartDttmStamp());
117                    if (calculatedMileageAmount.compareTo(document.getDvNonEmployeeTravel().getDisbVchrMileageCalculatedAmt()) != 0) {
118                        errors.putErrorWithoutFullErrorPath(KFSConstants.GENERAL_NONEMPLOYEE_TAB_ERRORS, KFSKeyConstants.ERROR_DV_MILEAGE_CALC_CHANGE);
119                        isValid = false;
120                    }
121    
122                    // determine if the rule is flagged off in the parm setting
123                    boolean performTravelMileageLimitInd = parameterService.getIndicatorParameter(DisbursementVoucherDocument.class, DisbursementVoucherConstants.NONEMPLOYEE_TRAVEL_ACTUAL_MILEAGE_LIMIT_PARM_NM);
124                    if (performTravelMileageLimitInd) {
125                        // if actual amount is greater than calculated amount
126                        if (currentCalcAmt.subtract(currentActualAmt).isNegative()) {
127                            errors.putError(KFSPropertyConstants.DV_PERSONAL_CAR_AMOUNT, KFSKeyConstants.ERROR_DV_ACTUAL_MILEAGE_TOO_HIGH);
128                            isValid = false;
129                        }
130                    }
131                }
132            }
133    
134            errors.removeFromErrorPath(KFSPropertyConstants.DV_NON_EMPLOYEE_TRAVEL);
135            errors.removeFromErrorPath(KFSPropertyConstants.DOCUMENT);
136    
137            return isValid;
138        }
139    
140        /**
141         * Determines if the given document has an income for tax
142         * @param document document to check
143         * @return true if it does have non-reportable income, false otherwise
144         */
145        protected boolean hasIncomeClassCode(DisbursementVoucherDocument document) {
146            return StringUtils.isNotBlank(document.getDvNonResidentAlienTax().getIncomeClassCode());
147        }
148        
149        /**
150         * Determines if the tax on the document was gross up
151         * @param document the document to check
152         * @return true if the tax was gross up, false otherwise
153         */
154        protected boolean isGrossUp(DisbursementVoucherDocument document) {
155            return document.getDvNonResidentAlienTax().isIncomeTaxGrossUpCode();
156        }
157        
158        /**
159         * Determines if tax should be taken into consideration when checking the total travel amount, and validates that it matches the paid amount
160         * @param document the document to validate the non-employee total travel amount of
161         * @return true if the document validated perfectly, false otherwise
162         */
163        protected boolean validateTravelAmount(DisbursementVoucherDocument document) {
164            /* total on non-employee travel must equal Check Total */
165            KualiDecimal paidAmount = document.getDisbVchrCheckTotalAmount();
166            final boolean incomeClassCoded = hasIncomeClassCode(document);
167            final boolean grossUp = isGrossUp(document);
168            final KualiDecimal travelAmount = document.getDvNonEmployeeTravel().getTotalTravelAmount();
169            if (incomeClassCoded && !grossUp) {  // we're adding tax and not grossing up; we need to add the tax amount to the paid amount
170                paidAmount = paidAmount.add(getDisbursementVoucherTaxService().getNonResidentAlienTaxAmount(document));
171            }
172            if (paidAmount.compareTo(travelAmount) != 0) {
173                GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KFSConstants.DV_CHECK_TRAVEL_TOTAL_ERROR, KFSKeyConstants.ERROR_DV_TRAVEL_CHECK_TOTAL);
174                return false;
175            }
176            return true;
177        }
178        
179        /**
180         * This method checks to see if the per diem section of the non employee travel tab contains any values. If this section
181         * contains any values, the section is validated to ensure that all the required fields for this section are populated.
182         * 
183         * @param document submitted disbursement voucher document
184         * @param errors map containing any generated errors 
185         * @return true if per diem section is used by user and that all fields contain values.
186         */
187        private boolean validatePerDiemSection(DisbursementVoucherDocument document, MessageMap errors) {
188            boolean perDiemSectionComplete = true;
189    
190            // Checks to see if any per diem fields are filled in
191            boolean perDiemUsed = StringUtils.isNotBlank(document.getDvNonEmployeeTravel().getDisbVchrPerdiemCategoryName()) || ObjectUtils.isNotNull(document.getDvNonEmployeeTravel().getDisbVchrPerdiemRate()) || ObjectUtils.isNotNull(document.getDvNonEmployeeTravel().getDisbVchrPerdiemCalculatedAmt()) || ObjectUtils.isNotNull(document.getDvNonEmployeeTravel().getDisbVchrPerdiemActualAmount());
192    
193            // If any per diem fields contain data, validates that all required per diem fields are filled in
194            if (perDiemUsed) {
195                if (StringUtils.isBlank(document.getDvNonEmployeeTravel().getDisbVchrPerdiemCategoryName())) {
196                    errors.putError(KFSPropertyConstants.DISB_VCHR_PERDIEM_CATEGORY_NAME, KFSKeyConstants.ERROR_DV_PER_DIEM_CATEGORY);
197                    perDiemSectionComplete = false;
198                }
199                if (ObjectUtils.isNull(document.getDvNonEmployeeTravel().getDisbVchrPerdiemRate())) {
200                    errors.putError(KFSPropertyConstants.DISB_VCHR_PERDIEM_RATE, KFSKeyConstants.ERROR_DV_PER_DIEM_RATE);
201                    perDiemSectionComplete = false;
202                }
203                if (ObjectUtils.isNull(document.getDvNonEmployeeTravel().getDisbVchrPerdiemCalculatedAmt())) {
204                    errors.putError(KFSPropertyConstants.DISB_VCHR_PERDIEM_CALCULATED_AMT, KFSKeyConstants.ERROR_DV_PER_DIEM_CALC_AMT);
205                    perDiemSectionComplete = false;
206                }
207                if (ObjectUtils.isNull(document.getDvNonEmployeeTravel().getDisbVchrPerdiemActualAmount())) {
208                    errors.putError(KFSPropertyConstants.DISB_VCHR_PERDIEM_ACTUAL_AMOUNT, KFSKeyConstants.ERROR_DV_PER_DIEM_ACTUAL_AMT);
209                    perDiemSectionComplete = false;
210                }
211            }
212            perDiemSectionComplete = perDiemSectionComplete && perDiemUsed;
213            return perDiemSectionComplete;
214        }
215    
216        /**
217         * This method checks to see if the per diem section of the non employee travel tab contains any values. If this section
218         * contains any values, the section is validated to ensure that all the required fields for this section are populated.
219         * 
220         * @param document submitted disbursement voucher document
221         * @param errors map containing any generated errors 
222         * @return true if per diem section is used by user and that all fields contain values.
223         */
224        private boolean validatePersonalVehicleSection(DisbursementVoucherDocument document, MessageMap errors) {
225            boolean personalVehicleSectionComplete = true;
226    
227            // Checks to see if any per diem fields are filled in
228            boolean personalVehilcleUsed = ObjectUtils.isNotNull(document.getDvNonEmployeeTravel().getDisbVchrAutoFromCityName()) || ObjectUtils.isNotNull(document.getDvNonEmployeeTravel().getDisbVchrAutoFromStateCode()) || ObjectUtils.isNotNull(document.getDvNonEmployeeTravel().getDisbVchrAutoToCityName()) || ObjectUtils.isNotNull(document.getDvNonEmployeeTravel().getDisbVchrAutoToStateCode()) || ObjectUtils.isNotNull(document.getDvNonEmployeeTravel().getDvPersonalCarMileageAmount()) || ObjectUtils.isNotNull(document.getDvNonEmployeeTravel().getDisbVchrMileageCalculatedAmt()) || ObjectUtils.isNotNull(document.getDvNonEmployeeTravel().getDisbVchrPersonalCarAmount());
229    
230    
231            // If any per diem fields contain data, validates that all required per diem fields are filled in
232            if (personalVehilcleUsed) {
233                if (ObjectUtils.isNull(document.getDvNonEmployeeTravel().getDisbVchrAutoFromCityName())) {
234                    errors.putError(KFSPropertyConstants.DISB_VCHR_AUTO_FROM_CITY_NAME, KFSKeyConstants.ERROR_DV_AUTO_FROM_CITY);
235                    personalVehicleSectionComplete = false;
236                }
237                if (ObjectUtils.isNull(document.getDvNonEmployeeTravel().getDisbVchrAutoToCityName())) {
238                    errors.putError(KFSPropertyConstants.DISB_VCHR_AUTO_TO_CITY_NAME, KFSKeyConstants.ERROR_DV_AUTO_TO_CITY);
239                    personalVehicleSectionComplete = false;
240                }
241    
242                // are state fields required always or only for US travel?
243                if (ObjectUtils.isNull(document.getDvNonEmployeeTravel().getDisbVchrAutoFromStateCode())) {
244                    errors.putError(KFSPropertyConstants.DISB_VCHR_AUTO_FROM_STATE_CODE, KFSKeyConstants.ERROR_DV_AUTO_FROM_STATE);
245                    personalVehicleSectionComplete = false;
246                }
247                if (ObjectUtils.isNull(document.getDvNonEmployeeTravel().getDisbVchrAutoToStateCode())) {
248                    errors.putError(KFSPropertyConstants.DISB_VCHR_AUTO_TO_STATE_CODE, KFSKeyConstants.ERROR_DV_AUTO_TO_STATE);
249                    personalVehicleSectionComplete = false;
250                }
251                // end state field validation
252    
253    
254                if (ObjectUtils.isNull(document.getDvNonEmployeeTravel().getDvPersonalCarMileageAmount())) {
255                    errors.putError(KFSPropertyConstants.DV_PERSONAL_CAR_MILEAGE_AMOUNT, KFSKeyConstants.ERROR_DV_MILEAGE_AMT);
256                    personalVehicleSectionComplete = false;
257                }
258                if (ObjectUtils.isNull(document.getDvNonEmployeeTravel().getDisbVchrMileageCalculatedAmt())) {
259                    errors.putError(KFSPropertyConstants.DISB_VCHR_MILEAGE_CALCULATED_AMT, KFSKeyConstants.ERROR_DV_MILEAGE_CALC_AMT);
260                    personalVehicleSectionComplete = false;
261                }
262                if (ObjectUtils.isNull(document.getDvNonEmployeeTravel().getDisbVchrPersonalCarAmount())) {
263                    errors.putError(KFSPropertyConstants.DISB_VCHR_PERSONAL_CAR_AMOUNT, KFSKeyConstants.ERROR_DV_MILEAGE_ACTUAL_AMT);
264                    personalVehicleSectionComplete = false;
265                }
266            }
267            personalVehicleSectionComplete = personalVehicleSectionComplete && personalVehilcleUsed;
268            return personalVehicleSectionComplete;
269        }
270    
271        /**
272         * Returns whether the document's payment reason is for travel by a non-employee
273         * 
274         * @param disbursementVoucherDocument submitted disbursement voucher document
275         * @return true if payment reason is travel by a non-employee
276         * 
277         */
278        private boolean isTravelNonEmplPaymentReason(DisbursementVoucherDocument disbursementVoucherDocument) {
279            ParameterEvaluator travelNonEmplPaymentReasonEvaluator = parameterService.getParameterEvaluator(DisbursementVoucherDocument.class, DisbursementVoucherConstants.NONEMPLOYEE_TRAVEL_PAY_REASONS_PARM_NM, disbursementVoucherDocument.getDvPayeeDetail().getDisbVchrPaymentReasonCode());
280            return travelNonEmplPaymentReasonEvaluator.evaluationSucceeds();
281        }
282    
283        /**
284         * Sets the accountingDocumentForValidation attribute value.
285         * 
286         * @param accountingDocumentForValidation The accountingDocumentForValidation to set.
287         */
288        public void setAccountingDocumentForValidation(AccountingDocument accountingDocumentForValidation) {
289            this.accountingDocumentForValidation = accountingDocumentForValidation;
290        }
291    
292        /**
293         * Sets the parameterService attribute value.
294         * @param parameterService The parameterService to set.
295         */
296        public void setParameterService(ParameterService parameterService) {
297            this.parameterService = parameterService;
298        }
299    
300        /**
301         * Gets the accountingDocumentForValidation attribute. 
302         * @return Returns the accountingDocumentForValidation.
303         */
304        public AccountingDocument getAccountingDocumentForValidation() {
305            return accountingDocumentForValidation;
306        }
307    
308        /**
309         * Gets the disbursementVoucherTaxService attribute. 
310         * @return Returns the disbursementVoucherTaxService.
311         */
312        public DisbursementVoucherTaxService getDisbursementVoucherTaxService() {
313            return disbursementVoucherTaxService;
314        }
315    
316        /**
317         * Sets the disbursementVoucherTaxService attribute value.
318         * @param disbursementVoucherTaxService The disbursementVoucherTaxService to set.
319         */
320        public void setDisbursementVoucherTaxService(DisbursementVoucherTaxService disbursementVoucherTaxService) {
321            this.disbursementVoucherTaxService = disbursementVoucherTaxService;
322        }
323    
324        /**
325         * Gets the disbursementVoucherTravelService attribute. 
326         * @return Returns the disbursementVoucherTravelService.
327         */
328        public DisbursementVoucherTravelService getDisbursementVoucherTravelService() {
329            return disbursementVoucherTravelService;
330        }
331    
332        /**
333         * Sets the disbursementVoucherTravelService attribute value.
334         * @param disbursementVoucherTravelService The disbursementVoucherTravelService to set.
335         */
336        public void setDisbursementVoucherTravelService(DisbursementVoucherTravelService disbursementVoucherTravelService) {
337            this.disbursementVoucherTravelService = disbursementVoucherTravelService;
338        }
339    
340        /**
341         * Gets the dictionaryValidationService attribute. 
342         * @return Returns the dictionaryValidationService.
343         */
344        public DictionaryValidationService getDictionaryValidationService() {
345            return dictionaryValidationService;
346        }
347    
348        /**
349         * Sets the dictionaryValidationService attribute value.
350         * @param dictionaryValidationService The dictionaryValidationService to set.
351         */
352        public void setDictionaryValidationService(DictionaryValidationService dictionaryValidationService) {
353            this.dictionaryValidationService = dictionaryValidationService;
354        }
355        
356    }