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 }