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 }