/*
* The Kuali Financial System, a comprehensive financial management system for higher education.
*
* Copyright 2005-2014 The Kuali Foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kuali.kfs.module.tem.document.validation.impl;
import java.math.BigDecimal;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.module.tem.TemKeyConstants;
import org.kuali.kfs.module.tem.TemPropertyConstants;
import org.kuali.kfs.module.tem.businessobject.ActualExpense;
import org.kuali.kfs.module.tem.businessobject.ExpenseType;
import org.kuali.kfs.module.tem.businessobject.ExpenseTypeObjectCode;
import org.kuali.kfs.module.tem.businessobject.PerDiemExpense;
import org.kuali.kfs.module.tem.businessobject.TemExpense;
import org.kuali.kfs.module.tem.document.TravelDocument;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.document.validation.event.AttributedDocumentEvent;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.krad.util.GlobalVariables;
import org.kuali.rice.krad.util.ObjectUtils;
public class TravelDocumentActualExpenseLineValidation extends TemDocumentExpenseLineValidation {
protected ActualExpense actualExpenseForValidation;
protected boolean currentExpenseInCollection = true;
/**
* @see org.kuali.kfs.sys.document.validation.Validation#validate(org.kuali.kfs.sys.document.validation.event.AttributedDocumentEvent)
*/
@Override
public boolean validate(AttributedDocumentEvent event) {
boolean success = true;
TravelDocument document = (TravelDocument)event.getDocument();
success = getDictionaryValidationService().isBusinessObjectValid(getActualExpenseForValidation());
if (success) {
success = validateExpenses(getActualExpenseForValidation(), document);
}
return success;
}
/**
* Validate the expense
*
* @param expense
* @param document
* @return
*/
public boolean validateExpenses(ActualExpense expense, TravelDocument document) {
boolean success = (validateGeneralRules(expense, document) &&
validateExpenseDetail(expense) &&
validateAirfareRules(expense, document) &&
validateRentalCarRules(expense, document) &&
validateLodgingRules(expense, document) &&
validateLodgingAllowanceRules(expense, document) &&
validatePerDiemRules(expense, document) &&
validateMaximumAmountRules(expense, document));
return success;
}
/**
* This method validates following rules
*
* 1.Validates notes field if indicator set to true
* 2.Validates expense amount if expense type is not mileage
* 3.Validates currency rate
* 4.Validates duplicate entries
*
* @param actualExpense
* @param document
* @return boolean
*/
public boolean validateGeneralRules(ActualExpense actualExpense, TravelDocument document) {
boolean success = true;
if (ObjectUtils.isNull(actualExpense)) {
return false;
}
final ExpenseTypeObjectCode expenseTypeCode = actualExpense.getExpenseTypeObjectCode();
//validate expense amount greater than 0
if (!actualExpense.isMileage()) {
if (actualExpense.getExpenseAmount().isNegative()) {
GlobalVariables.getMessageMap().putError(TemPropertyConstants.EXPENSE_AMOUNT, KFSKeyConstants.ERROR_ZERO_OR_NEGATIVE_AMOUNT, "Expense Amount");
success = false;
}
}
//validate currency rate is greater than 0
if (actualExpense.getCurrencyRate().compareTo(BigDecimal.ZERO) < 0) {
GlobalVariables.getMessageMap().putError(TemPropertyConstants.CURRENCY_RATE, KFSKeyConstants.ERROR_ZERO_OR_NEGATIVE_AMOUNT, "Currency Rate");
success = false;
}
//validate duplicate entry
if (isDuplicateEntry(actualExpense, document)) {
success = false;
if (expenseTypeCode != null && expenseTypeCode.isPerDaily()) {
GlobalVariables.getMessageMap().putError(TemPropertyConstants.EXPENSE_AMOUNT, TemKeyConstants.ERROR_ACTUAL_EXPENSE_DUPLICATE_ENTRY_DAILY, actualExpense.getExpenseTypeCode());
}
else {
GlobalVariables.getMessageMap().putError(TemPropertyConstants.EXPENSE_AMOUNT, TemKeyConstants.ERROR_ACTUAL_EXPENSE_DUPLICATE_ENTRY, actualExpense.getExpenseTypeCode(), actualExpense.getExpenseDate().toString());
}
}
return success;
}
/**
* Validate if the expense is required to have detail to be entered. Detail requirement is defined in the travel expense type code
* table
*
* @param actualExpense
* @param isWarning
* @return
*/
public boolean validateExpenseDetail(ActualExpense actualExpense) {
boolean success = true;
actualExpense.refreshReferenceObject(TemPropertyConstants.EXPENSE_TYPE_OBJECT_CODE);
ExpenseTypeObjectCode expenseType = actualExpense.getExpenseTypeObjectCode();
if (ObjectUtils.isNotNull(expenseType)){
if (expenseType.getExpenseType().isExpenseDetailRequired() && actualExpense.getExpenseDetails().isEmpty()){
//detail is required when adding the expense
if (isWarningOnly()){
GlobalVariables.getMessageMap().putWarning(TemPropertyConstants.TEM_ACTUAL_EXPENSE_DETAIL, TemKeyConstants.ERROR_ACTUAL_EXPENSE_DETAIL_REQUIRED, expenseType.getExpenseType().getName());
}else{
GlobalVariables.getMessageMap().putError(TemPropertyConstants.TEM_ACTUAL_EXPENSE_DETAIL, TemKeyConstants.ERROR_ACTUAL_EXPENSE_DETAIL_REQUIRED, expenseType.getExpenseType().getName());
success = false;
}
}
}
return success;
}
/**
* This method validates following rules 1.Validates user entered amount with max amount & max amount per configured in
* database(daily & occurrence).
*
* @param actualExpense
* @param document
* @return boolean
*/
public boolean validateMaximumAmountRules(ActualExpense actualExpense, TravelDocument document) {
boolean success = true;
ExpenseTypeObjectCode expenseTypeObjectCode = actualExpense.getExpenseTypeObjectCode();
KualiDecimal maxAmount = getMaximumAmount(actualExpense, document, expenseTypeObjectCode);
if (maxAmount.isNonZero()) {
if (expenseTypeObjectCode.isPerDaily()) {
if (maxAmount.isLessThan(actualExpense.getConvertedAmount())) { // per daily - check that just this actual expense is greater than max amount
success = false;
GlobalVariables.getMessageMap().putError(TemPropertyConstants.EXPENSE_AMOUNT, TemKeyConstants.ERROR_ACTUAL_EXPENSE_MAX_AMT_PER_DAILY, expenseTypeObjectCode.getMaximumAmount().toString());
}
}
else if (expenseTypeObjectCode.isPerOccurrence()) {
KualiDecimal totalPerExpenseType = getTotalDocumentAmountForExpenseType(document, actualExpense.getExpenseType());
if (!isCurrentExpenseInCollection()) {
totalPerExpenseType = totalPerExpenseType.add(actualExpense.getConvertedAmount());
}
if (maxAmount.isLessThan(totalPerExpenseType)) {
success = false;
GlobalVariables.getMessageMap().putError(TemPropertyConstants.EXPENSE_AMOUNT, TemKeyConstants.ERROR_ACTUAL_EXPENSE_MAX_AMT_PER_OCCU, expenseTypeObjectCode.getMaximumAmount().toString());
}
}
}
return success;
}
/**
* Calculates the total of all actual and imported expenses on the document with the given expense type
* @param document the document to find the total of expenses of the given expense type for
* @param expenseType the expense type to find a total for
* @return the total of the converted values of all expenses on the document of that expense type
*/
protected KualiDecimal getTotalDocumentAmountForExpenseType(TravelDocument document, ExpenseType expenseType) {
final KualiDecimal total = getTotalForExpenseType(document.getActualExpenses(), expenseType).add(getTotalForExpenseType(document.getImportedExpenses(), expenseType));
return total;
}
/**
* Given a list of expenses, calculates the total of those expenses where the expense type of the expense is that of the given expense type
* @param expenses the expenses to total
* @param expenseType the expense type to total for
* @return the total of the expenses for the given expense type
*/
protected KualiDecimal getTotalForExpenseType(List<? extends TemExpense> expenses, ExpenseType expenseType) {
KualiDecimal total = KualiDecimal.ZERO;
for (TemExpense expense : expenses) {
if (StringUtils.equals(expense.getExpenseTypeCode(), expenseType.getCode())) {
total = total.add(expense.getConvertedAmount());
}
}
return total;
}
/**
* This method validates following rules
* <ol>
* <li>Validates if lodging allowance is entered for the same day</li>
* <li>Per Diem lodging allowance can also not be entered on the same day</li>
* </ol>
*
* Warn if there is any lodging allowance in the Per Diem section
*
* @param actualExpense
* @param document
* @return boolean
*/
public boolean validateLodgingRules(ActualExpense actualExpense, TravelDocument document) {
boolean success = true;
if (actualExpense.isLodging()) {
if (isLodgingAllowanceEntered(actualExpense, document)) {
success = false;
GlobalVariables.getMessageMap().putError(TemPropertyConstants.CURRENCY_RATE, TemKeyConstants.ERROR_ACTUAL_EXPENSE_LODGING_ENTERED);
}
}
return success;
}
/**
* This method validated following rules
*
* 1. Validates if lodging allowance is entered for the same day
* 2. Per Diem lodging allowance can also not be entered on the same day
*
* Warn if there is any lodging allowance in the Per Diem section
*
* @param actualExpense
* @param document
* @return boolean
*/
public boolean validateLodgingAllowanceRules(ActualExpense actualExpense, TravelDocument document) {
boolean success = true;
if (actualExpense.isLodgingAllowance()) {
ExpenseTypeObjectCode expenseTypeCode = actualExpense.getExpenseTypeObjectCode();
KualiDecimal maxAmount = ObjectUtils.isNotNull(expenseTypeCode) && ObjectUtils.isNotNull(expenseTypeCode.getMaximumAmount()) ? expenseTypeCode.getMaximumAmount() : KualiDecimal.ZERO;
GlobalVariables.getMessageMap().putInfo(TemPropertyConstants.TEM_ACTUAL_EXPENSE_NOTCE, TemKeyConstants.INFO_TEM_ACTUAL_EXPENSE_LODGING_ALLOWANCE, maxAmount.toString());
if (isLodgingEntered(actualExpense, document)) {
success = false;
GlobalVariables.getMessageMap().putError(TemPropertyConstants.EXPENSE_TYPE_CODE, TemKeyConstants.ERROR_ACTUAL_EXPENSE_LODGING_ENTERED);
}
if (isPerDiemLodgingEntered(actualExpense.getExpenseDate(), document.getPerDiemExpenses())) {
GlobalVariables.getMessageMap().putWarning(TemPropertyConstants.TEM_ACTUAL_EXPENSE_NOTCE, TemKeyConstants.WARNING_PERDIEM_EXPENSE_LODGING_ENTERED);
}
}
return success;
}
/**
*
* @param ote
* @param document
* @return
*/
protected boolean isLodgingAllowanceEntered(ActualExpense ote, TravelDocument document) {
for (ActualExpense actualExpense : document.getActualExpenses()) {
if (actualExpense.isLodgingAllowance()
&& (!ote.equals(actualExpense))
&& (ote.getExpenseDate() != null
&& ote.getExpenseDate().equals(actualExpense.getExpenseDate()))) {
return true;
}
}
for (PerDiemExpense perDiemExpense : document.getPerDiemExpenses()) {
if (KualiDecimal.ZERO.isLessThan(perDiemExpense.getLodging())
&& (ote.getExpenseDate() != null
&& ote.getExpenseDate().equals(perDiemExpense.getMileageDate()))) {
return true;
}
}
return false;
}
/**
*
* @param ote
* @param document
* @return
*/
protected boolean isLodgingEntered(ActualExpense ote, TravelDocument document) {
for (ActualExpense actualExpense : document.getActualExpenses()) {
if (actualExpense.isLodging()
&& (!ote.equals(actualExpense))
&& (ote.getExpenseDate() != null
&& ote.getExpenseDate().equals(actualExpense.getExpenseDate()))) {
return true;
}
}
return false;
}
/**
* Checking duplicate on expense (non-details) if they are duplicates
*
* @param ote
* @param document
* @return
*/
protected boolean isDuplicateEntry(ActualExpense expense, TravelDocument document) {
if (document.shouldRefreshExpenseTypeObjectCode()) {
expense.refreshExpenseTypeObjectCode(document.getDocumentTypeName(), document.getTraveler().getTravelerTypeCode(), document.getTripTypeCode());
}
final ExpenseTypeObjectCode expenseTypeCode = expense.getExpenseTypeObjectCode();
//do a check if its coming out of the document expense list - this will happen during route validation
if (!document.getActualExpenses().contains(expense)){
for (ActualExpense actualExpense : document.getActualExpenses()) {
if (expense.getExpenseDate() != null
&& expense.getExpenseDate().equals(actualExpense.getExpenseDate())
&& actualExpense.getExpenseTypeCode().equals(expense.getExpenseTypeCode())) {
return true;
}
}
}
return false;
}
/**
* Get maximum amount
*
* @param actualExpense
* @param document
* @return
*/
protected KualiDecimal getMaximumAmount(ActualExpense actualExpense, TravelDocument document, ExpenseTypeObjectCode expenseTypeObjectCode) {
KualiDecimal maxAmount = KualiDecimal.ZERO;
if (expenseTypeObjectCode != null && expenseTypeObjectCode.getMaximumAmount() != null) {
maxAmount = expenseTypeObjectCode.getMaximumAmount();
}
//add the group traveler list + self (1)
if (actualExpense.getExpenseType().isGroupTravel()) {
KualiDecimal groupTravelMultipier = new KualiDecimal(document.getGroupTravelers().size() + 1);
maxAmount = maxAmount.multiply(groupTravelMultipier);
}
return maxAmount;
}
/**
* @param ote
* @param document
* @return
*/
protected KualiDecimal getTotalExpenseAmount(ActualExpense ote, TravelDocument document) {
KualiDecimal totalExpenseAmount = KualiDecimal.ZERO;
for (ActualExpense actualExpense : document.getActualExpenses()) {
if ((!ote.equals(actualExpense)) && ote.getExpenseTypeCode().equals(actualExpense.getExpenseTypeCode())) {
totalExpenseAmount = totalExpenseAmount.add(actualExpense.getExpenseAmount());
}
}
return totalExpenseAmount.add(ote.getExpenseAmount());
}
public ActualExpense getActualExpenseForValidation() {
return actualExpenseForValidation;
}
public void setActualExpenseForValidation(ActualExpense actualExpenseForValidation) {
this.actualExpenseForValidation = actualExpenseForValidation;
}
public boolean isCurrentExpenseInCollection() {
return currentExpenseInCollection;
}
public void setCurrentExpenseInCollection(boolean currentExpenseInCollection) {
this.currentExpenseInCollection = currentExpenseInCollection;
}
}