org.kuali.kfs.module.tem.document.service.impl.TravelDocumentServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.kuali.kfs.module.tem.document.service.impl.TravelDocumentServiceImpl.java

Source

/*
 * 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.service.impl;

import static org.kuali.kfs.module.tem.TemKeyConstants.ERROR_UPLOADPARSER_INVALID_NUMERIC_VALUE;
import static org.kuali.kfs.module.tem.TemKeyConstants.ERROR_UPLOADPARSER_LINE;
import static org.kuali.kfs.module.tem.TemKeyConstants.ERROR_UPLOADPARSER_PROPERTY;
import static org.kuali.kfs.module.tem.TemKeyConstants.ERROR_UPLOADPARSER_WRONG_PROPERTY_NUMBER;
import static org.kuali.kfs.module.tem.TemKeyConstants.MESSAGE_TR_LODGING_ALREADY_CLAIMED;
import static org.kuali.kfs.module.tem.TemKeyConstants.MESSAGE_TR_MEAL_ALREADY_CLAIMED;
import static org.kuali.kfs.module.tem.TemKeyConstants.MESSAGE_UPLOADPARSER_EXCEEDED_MAX_LENGTH;
import static org.kuali.kfs.module.tem.TemKeyConstants.MESSAGE_UPLOADPARSER_INVALID_VALUE;
import static org.kuali.kfs.module.tem.TemPropertyConstants.PER_DIEM_EXPENSE_DISABLED;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Timestamp;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.time.DateUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.integration.ar.AccountsReceivableCustomerInvoice;
import org.kuali.kfs.integration.ar.AccountsReceivableModuleService;
import org.kuali.kfs.integration.ar.AccountsReceivableOrganizationOptions;
import org.kuali.kfs.module.tem.TemConstants;
import org.kuali.kfs.module.tem.TemConstants.TravelAuthorizationParameters;
import org.kuali.kfs.module.tem.TemConstants.TravelAuthorizationStatusCodeKeys;
import org.kuali.kfs.module.tem.TemConstants.TravelDocTypes;
import org.kuali.kfs.module.tem.TemConstants.TravelParameters;
import org.kuali.kfs.module.tem.TemKeyConstants;
import org.kuali.kfs.module.tem.TemParameterConstants;
import org.kuali.kfs.module.tem.TemPropertyConstants;
import org.kuali.kfs.module.tem.TemWorkflowConstants;
import org.kuali.kfs.module.tem.businessobject.ActualExpense;
import org.kuali.kfs.module.tem.businessobject.ExpenseType;
import org.kuali.kfs.module.tem.businessobject.ExpenseTypeAware;
import org.kuali.kfs.module.tem.businessobject.GroupTraveler;
import org.kuali.kfs.module.tem.businessobject.GroupTravelerCsvRecord;
import org.kuali.kfs.module.tem.businessobject.HistoricalTravelExpense;
import org.kuali.kfs.module.tem.businessobject.ImportedExpense;
import org.kuali.kfs.module.tem.businessobject.MileageRate;
import org.kuali.kfs.module.tem.businessobject.PerDiem;
import org.kuali.kfs.module.tem.businessobject.PerDiemExpense;
import org.kuali.kfs.module.tem.businessobject.PrimaryDestination;
import org.kuali.kfs.module.tem.businessobject.SpecialCircumstances;
import org.kuali.kfs.module.tem.businessobject.SpecialCircumstancesQuestion;
import org.kuali.kfs.module.tem.businessobject.TemExpense;
import org.kuali.kfs.module.tem.businessobject.TemRegion;
import org.kuali.kfs.module.tem.businessobject.TemSourceAccountingLine;
import org.kuali.kfs.module.tem.businessobject.TransportationModeDetail;
import org.kuali.kfs.module.tem.businessobject.TravelAdvance;
import org.kuali.kfs.module.tem.businessobject.TripType;
import org.kuali.kfs.module.tem.dataaccess.TravelDocumentDao;
import org.kuali.kfs.module.tem.document.TEMReimbursementDocument;
import org.kuali.kfs.module.tem.document.TravelAuthorizationDocument;
import org.kuali.kfs.module.tem.document.TravelDocument;
import org.kuali.kfs.module.tem.document.TravelEntertainmentDocument;
import org.kuali.kfs.module.tem.document.TravelReimbursementDocument;
import org.kuali.kfs.module.tem.document.TravelRelocationDocument;
import org.kuali.kfs.module.tem.document.service.AccountingDocumentRelationshipService;
import org.kuali.kfs.module.tem.document.service.MileageRateService;
import org.kuali.kfs.module.tem.document.service.TravelAuthorizationService;
import org.kuali.kfs.module.tem.document.service.TravelDocumentService;
import org.kuali.kfs.module.tem.document.web.struts.TravelFormBase;
import org.kuali.kfs.module.tem.exception.UploadParserException;
import org.kuali.kfs.module.tem.service.CsvRecordFactory;
import org.kuali.kfs.module.tem.service.PerDiemService;
import org.kuali.kfs.module.tem.service.TemRoleService;
import org.kuali.kfs.module.tem.service.TravelExpenseService;
import org.kuali.kfs.module.tem.service.TravelService;
import org.kuali.kfs.module.tem.util.ExpenseUtils;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.AccountingLine;
import org.kuali.kfs.sys.businessobject.FinancialSystemDocumentHeader;
import org.kuali.kfs.sys.businessobject.PaymentDocumentationLocation;
import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.exception.ParseException;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.kfs.sys.util.KfsDateUtils;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.ConcreteKeyValue;
import org.kuali.rice.core.api.util.KeyValue;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.core.web.format.FormatException;
import org.kuali.rice.coreservice.framework.parameter.ParameterService;
import org.kuali.rice.kew.api.KewApiConstants;
import org.kuali.rice.kew.api.KewApiServiceLocator;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kew.api.action.ActionRequestType;
import org.kuali.rice.kew.api.document.attribute.DocumentAttributeIndexingQueue;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kim.api.identity.Person;
import org.kuali.rice.kim.api.identity.PersonService;
import org.kuali.rice.kim.api.identity.principal.Principal;
import org.kuali.rice.kim.api.services.KimApiServiceLocator;
import org.kuali.rice.kns.document.authorization.DocumentAuthorizer;
import org.kuali.rice.kns.service.DocumentHelperService;
import org.kuali.rice.kns.util.KNSGlobalVariables;
import org.kuali.rice.krad.bo.AdHocRoutePerson;
import org.kuali.rice.krad.bo.Note;
import org.kuali.rice.krad.bo.PersistableBusinessObject;
import org.kuali.rice.krad.document.Document;
import org.kuali.rice.krad.exception.InfrastructureException;
import org.kuali.rice.krad.service.BusinessObjectService;
import org.kuali.rice.krad.service.DataDictionaryService;
import org.kuali.rice.krad.service.DocumentService;
import org.kuali.rice.krad.service.NoteService;
import org.kuali.rice.krad.service.SequenceAccessorService;
import org.kuali.rice.krad.uif.field.LinkField;
import org.kuali.rice.krad.util.GlobalVariables;
import org.kuali.rice.krad.util.KRADPropertyConstants;
import org.kuali.rice.krad.util.ObjectUtils;
import org.kuali.rice.location.api.state.State;
import org.kuali.rice.location.api.state.StateService;
import org.springframework.beans.BeanUtils;
import org.springframework.transaction.annotation.Transactional;

import au.com.bytecode.opencsv.CSVReader;

/**
 * Travel Service Implementation
 */
@Transactional
public class TravelDocumentServiceImpl implements TravelDocumentService {

    protected static Logger LOG = Logger.getLogger(TravelDocumentServiceImpl.class);

    protected DataDictionaryService dataDictionaryService;
    protected DocumentService documentService;
    protected BusinessObjectService businessObjectService;
    protected TravelDocumentDao travelDocumentDao;
    protected TravelAuthorizationService travelAuthorizationService;
    protected DateTimeService dateTimeService;
    protected ParameterService parameterService;
    protected AccountingDocumentRelationshipService accountingDocumentRelationshipService;
    protected TemRoleService temRoleService;
    protected StateService stateService;
    protected ConfigurationService configurationService;
    protected UniversityDateService universityDateService;
    protected List<String> defaultAcceptableFileExtensions;
    protected CsvRecordFactory<GroupTravelerCsvRecord> csvRecordFactory;
    protected List<String> groupTravelerColumns;
    protected volatile AccountsReceivableModuleService accountsReceivableModuleService;
    protected PerDiemService perDiemService;
    protected TravelExpenseService travelExpenseService;
    protected NoteService noteService;
    protected TravelService travelService;
    protected MileageRateService mileageRateService;

    /**
     * Creates and populates an individual per diem item.
     *
     * @param perDiemId is the id for the referenced {@link PerDiem} object that gets attached
     * @return date of the item
     */
    protected PerDiemExpense createPerDiemItem(final TravelDocument document, final PerDiem newPerDiem,
            final Timestamp ts, final boolean prorated, String mileageRateExpenseTypeCode) {
        final PerDiemExpense expense = newPerDiemExpense();
        expense.setPrimaryDestinationId(newPerDiem.getPrimaryDestinationId());
        expense.setProrated(prorated);
        expense.setMileageDate(ts);

        expense.setPrimaryDestination(newPerDiem.getPrimaryDestination().getPrimaryDestinationName());
        expense.setCountryState(newPerDiem.getPrimaryDestination().getRegion().getRegionName());
        expense.setCounty(newPerDiem.getPrimaryDestination().getCounty());

        setPerDiemMealsAndIncidentals(expense, newPerDiem, document.getTripType(), document.getTripEnd(),
                expense.isProrated());
        final KualiDecimal lodgingAmount = getPerDiemService().isPerDiemHandlingLodging()
                && !KfsDateUtils.isSameDay(document.getTripEnd(), ts) ? newPerDiem.getLodging() : KualiDecimal.ZERO;
        expense.setLodging(lodgingAmount);
        expense.setMileageRateExpenseTypeCode(mileageRateExpenseTypeCode);
        return expense;
    }

    /**
     * returns a new instance of a PerDiemExpense turned into a service call so that we can provide our own instance during testing
     */
    protected PerDiemExpense newPerDiemExpense() {
        return new PerDiemExpense();
    }

    /**
     * Sets the meal and incidental amounts on the given per diem expense
     * @param expense the expense to set amounts on
     * @param perDiem the per diem record amounts are based off of
     * @param tripType the trip type being taken
     * @param tripEnd the end time of the trip
     * @param shouldProrate whether this expense should be prorated
     */
    @Override
    public void setPerDiemMealsAndIncidentals(PerDiemExpense expense, PerDiem perDiem, TripType tripType,
            Timestamp tripEnd, boolean shouldProrate) {
        String perDiemCalcMethod = null;
        if (!ObjectUtils.isNull(tripType)) {
            perDiemCalcMethod = tripType.getPerDiemCalcMethod();
        }
        //default first to per diem's values
        expense.setBreakfastValue(perDiem.getBreakfast());
        expense.setLunchValue(perDiem.getLunch());
        expense.setDinnerValue(perDiem.getDinner());
        expense.setIncidentalsValue(perDiem.getIncidentals());
        // if prorated, recalculate the values
        if (shouldProrate) {
            Integer perDiemPercent = calculateProratePercentage(expense, perDiemCalcMethod, tripEnd);
            expense.setDinnerValue(
                    PerDiemExpense.calculateMealsAndIncidentalsProrated(expense.getDinnerValue(), perDiemPercent));
            expense.setLunchValue(
                    PerDiemExpense.calculateMealsAndIncidentalsProrated(expense.getLunchValue(), perDiemPercent));
            expense.setBreakfastValue(PerDiemExpense
                    .calculateMealsAndIncidentalsProrated(expense.getBreakfastValue(), perDiemPercent));
            expense.setIncidentalsValue(PerDiemExpense
                    .calculateMealsAndIncidentalsProrated(expense.getIncidentalsValue(), perDiemPercent));

            correctProratedPerDiemExpense(expense, perDiemPercent, perDiem);
        }
    }

    /**
     * Makes sure that any rounding in determining prorated meals or incidentals amounts will not be more than the meals and incidentals totals allowed by the per diem.
     * Extra change will be taken from breakfast.
     * @param expense the expense to correct
     * @param perDiemPercent the percentage of the proration for this per diem
     * @param perDiem the per diem record to work against
     */
    protected void correctProratedPerDiemExpense(PerDiemExpense expense, Integer perDiemPercent, PerDiem perDiem) {
        final KualiDecimal mealAndIncidentalLimit = PerDiemExpense
                .calculateMealsAndIncidentalsProrated(perDiem.getMealsAndIncidentals(), perDiemPercent);
        if (expense.getMealsAndIncidentals().isGreaterThan(mealAndIncidentalLimit)) {
            // take the difference from breakfast
            final KualiDecimal delta = expense.getMealsAndIncidentals().subtract(mealAndIncidentalLimit);
            expense.setBreakfastValue(expense.getBreakfastValue().subtract(delta));
        }
    }

    /**
     * Creates a date range for iterating over
     *
     * @param start of the date range
     * @param end of the date range
     * @return Collection for iterating
     */
    protected Collection<Timestamp> dateRange(final Timestamp start, final Timestamp end) {
        final Collection<Timestamp> retval = new ArrayList<Timestamp>();

        if (start != null && end != null) {
            final Calendar cal = getDateTimeService().getCurrentCalendar();
            cal.setTime(start);

            for (; !cal.getTime().after(end) || KfsDateUtils.isSameDay(cal.getTime(), end); cal.add(Calendar.DATE,
                    1)) {
                if (KfsDateUtils.isSameDay(cal.getTime(), end)) {
                    retval.add(new Timestamp(end.getTime()));
                } else {
                    retval.add(new Timestamp(cal.getTime().getTime()));
                }
            }
        }

        return retval;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#updatePerDiemItemsFor(String, List, Integer, Timestamp, Timestamp)
     */
    @SuppressWarnings("null")
    @Override
    public void updatePerDiemItemsFor(final TravelDocument document, final List<PerDiemExpense> perDiemExpenseList,
            final Integer perDiemId, final Timestamp start, final Timestamp end) {
        final String mileageRateExpenseTypeCode = getParameterService().getParameterValueAsString(
                TemParameterConstants.TEM_DOCUMENT.class,
                TemConstants.TravelParameters.PER_DIEM_MILEAGE_RATE_EXPENSE_TYPE_CODE, KFSConstants.EMPTY_STRING);

        // Check for changes on trip begin and trip end.
        // This is necessary to prevent duplication of per diem creation due to timestamp changes.
        boolean datesChanged = false;
        if (perDiemExpenseList != null && !perDiemExpenseList.isEmpty()) {
            Timestamp tempStart = perDiemExpenseList.get(0).getMileageDate();
            Timestamp tempEnd = perDiemExpenseList.get(0).getMileageDate();

            if (perDiemExpenseList.size() > 1) {
                tempEnd = perDiemExpenseList.get(perDiemExpenseList.size() - 1).getMileageDate();
            }

            if (!(tempStart.equals(start) && tempEnd.equals(end))) {
                // the perDiemExpenseList will be cleared once we recreate the table, but we need it for carrying over mileage rates
                datesChanged = true;
            }
        }

        List<PerDiem> perDiemList = new ArrayList<PerDiem>();

        // find a valid per diem for each date.  If per diem is null, make it a custom per diem.
        for (final Timestamp eachTimestamp : dateRange(start, end)) {
            PerDiem perDiem = getPerDiemService().getPerDiem(document.getPrimaryDestinationId(), eachTimestamp,
                    document.getEffectiveDateForPerDiem(eachTimestamp));
            if (perDiem == null
                    || perDiem.getPrimaryDestinationId() == TemConstants.CUSTOM_PRIMARY_DESTINATION_ID) {
                perDiem = new PerDiem();
                perDiem.setPrimaryDestination(new PrimaryDestination());
                perDiem.getPrimaryDestination().setRegion(new TemRegion());
                perDiem.getPrimaryDestination().getRegion().setTripType(new TripType());
                perDiem.setPrimaryDestinationId(TemConstants.CUSTOM_PRIMARY_DESTINATION_ID);
                perDiem.getPrimaryDestination().getRegion()
                        .setRegionName(document.getPrimaryDestinationCountryState());
                perDiem.getPrimaryDestination().setCounty(document.getPrimaryDestinationCounty());
                perDiem.getPrimaryDestination().getRegion().setTripType(document.getTripType());
                perDiem.getPrimaryDestination().getRegion().setTripTypeCode(document.getTripTypeCode());
                perDiem.getPrimaryDestination().setPrimaryDestinationName(document.getPrimaryDestinationName());
            }
            perDiemList.add(perDiem);
        }

        final Map<Timestamp, PerDiemExpense> perDiemMapped = new HashMap<Timestamp, PerDiemExpense>();

        int diffStartDays = 0;
        if (perDiemExpenseList.size() > 0 && perDiemExpenseList.get(0).getMileageDate() != null && !datesChanged) {
            diffStartDays = dateTimeService.dateDiff(start, perDiemExpenseList.get(0).getMileageDate(), false);
        }

        Calendar endCal = Calendar.getInstance();

        if (end != null) {
            endCal.setTime(end);
            if (!datesChanged) {
                for (final PerDiemExpense perDiemItem : perDiemExpenseList) {
                    if (diffStartDays != 0) {
                        Calendar cal = Calendar.getInstance();
                        cal.setTime(perDiemItem.getMileageDate());
                        cal.add(Calendar.DATE, -diffStartDays);
                        perDiemItem.setMileageDate(new Timestamp(cal.getTimeInMillis()));
                    }

                    if (perDiemItem.getMileageDate() != null) {
                        Calendar currCal = Calendar.getInstance();
                        currCal.setTime(perDiemItem.getMileageDate());
                        if (!endCal.before(currCal)) {
                            perDiemMapped.put(perDiemItem.getMileageDate(), perDiemItem);
                        }
                    }
                }
            }

            LOG.debug("Iterating over date range from " + start + " to " + end);
            int counter = 0;
            for (final Timestamp someDate : dateRange(start, end)) {
                // Check if a per diem entry exists for this date
                if (!perDiemMapped.containsKey(someDate)) {
                    final boolean prorated = shouldProrate(someDate, start, end);
                    PerDiemExpense perDiemExpense = createPerDiemItem(document, perDiemList.get(counter), someDate,
                            prorated, mileageRateExpenseTypeCode);
                    perDiemExpense.setDocumentNumber(document.getDocumentNumber());
                    perDiemMapped.put(someDate, perDiemExpense);
                }
                counter++;
            }
        }

        // Sort the dates and recreate the collection
        perDiemExpenseList.clear();
        for (final Timestamp someDate : new TreeSet<Timestamp>(perDiemMapped.keySet())) {
            LOG.debug("Adding " + perDiemMapped.get(someDate) + " to perdiem list");
            perDiemExpenseList.add(perDiemMapped.get(someDate));
        }
    }

    /**
     * Determines if per diem expenses on the given date should be prorated
     * @param perDiemDate the timestamp of the per diem
     * @param tripBegin the begin timestamp of the trip
     * @param tripEnd the end timestamp of the trip
     * @return true if the per diem expense should be prorated, false otherwise
     */
    protected boolean shouldProrate(Timestamp perDiemDate, Timestamp tripBegin, Timestamp tripEnd) {
        final boolean prorated = !KfsDateUtils.isSameDay(tripBegin, tripEnd)
                && (KfsDateUtils.isSameDay(perDiemDate, tripBegin) || KfsDateUtils.isSameDay(perDiemDate, tripEnd));
        return prorated;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getMileageRateKeyValues(java.sql.Date)
     */
    @Override
    public List<KeyValue> getMileageRateKeyValues(Date searchDate) {
        List<KeyValue> keyValues = new ArrayList<KeyValue>();

        TravelDocument document = (TravelDocument) ((TravelFormBase) KNSGlobalVariables.getKualiForm())
                .getDocument();
        String documentType = getDocumentType(document);
        final String travelerType = ObjectUtils.isNull(document.getTraveler()) ? null
                : document.getTraveler().getTravelerTypeCode();

        final Collection<ExpenseType> expenseTypes = getTravelExpenseService()
                .getExpenseTypesForDocument(documentType, document.getTripTypeCode(), travelerType, false);

        for (final ExpenseType expenseType : expenseTypes) {
            if (TemConstants.ExpenseTypeMetaCategory.MILEAGE.getCode()
                    .equals(expenseType.getExpenseTypeMetaCategoryCode())) {
                final MileageRate mileageRate = getMileageRateService()
                        .findMileageRateByExpenseTypeCodeAndDate(expenseType.getCode(), searchDate);
                if (mileageRate != null) {
                    keyValues.add(new ConcreteKeyValue(expenseType.getCode(),
                            expenseType.getCode() + " - " + mileageRate.getRate().toString()));
                }
            }
        }

        //sort by label
        Comparator<KeyValue> labelComparator = new Comparator<KeyValue>() {
            @Override
            public int compare(KeyValue o1, KeyValue o2) {
                return o1.getKey().compareTo(o2.getKey());
            }
        };

        Collections.sort(keyValues, labelComparator);

        return keyValues;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copyDownPerDiemExpense(int, java.util.List)
     */
    @Override
    public void copyDownPerDiemExpense(TravelDocument travelDocument, int copyIndex,
            List<PerDiemExpense> perDiemExpenses) {

        PerDiemExpense lineToCopy = perDiemExpenses.get(copyIndex);
        PerDiemExpense restoredLine = getRestoredPerDiemForCopying(travelDocument, lineToCopy);

        List<PerDiemExpense> tempPerDiemExpenses = new ArrayList<PerDiemExpense>();

        if (copyIndex < perDiemExpenses.size()) {
            for (int i = 0; i < perDiemExpenses.size(); i++) {
                PerDiemExpense perDiemExpense = new PerDiemExpense();
                if (perDiemExpenses != null && i < copyIndex) {
                    // copy over from the old list
                    perDiemExpense = perDiemExpenses.get(i);
                } else if (i > copyIndex) {
                    perDiemExpense = copyPerDiemExpense(restoredLine);
                    perDiemExpense.setMileageDate(perDiemExpenses.get(i).getMileageDate());
                    if (shouldProrate(perDiemExpense.getMileageDate(), travelDocument.getTripBegin(),
                            travelDocument.getTripEnd())) {
                        // prorate
                        perDiemExpense.setProrated(true);
                        if (perDiemExpense
                                .getPrimaryDestinationId() == TemConstants.CUSTOM_PRIMARY_DESTINATION_ID) {
                            // prorate the restored line to create new per diem
                            final PerDiem perDiem = copyIntoPerDiem(restoredLine);
                            final Integer perDiemPercent = lookupProratePercentage(perDiemExpense,
                                    travelDocument.getTripType().getPerDiemCalcMethod(),
                                    travelDocument.getTripEnd());
                            perDiemExpense.setDinnerValue(PerDiemExpense.calculateMealsAndIncidentalsProrated(
                                    perDiemExpense.getDinnerValue(), perDiemPercent));
                            perDiemExpense.setLunchValue(PerDiemExpense.calculateMealsAndIncidentalsProrated(
                                    perDiemExpense.getLunchValue(), perDiemPercent));
                            perDiemExpense.setBreakfastValue(PerDiemExpense.calculateMealsAndIncidentalsProrated(
                                    perDiemExpense.getBreakfastValue(), perDiemPercent));
                            perDiemExpense.setIncidentalsValue(PerDiemExpense.calculateMealsAndIncidentalsProrated(
                                    perDiemExpense.getIncidentalsValue(), perDiemPercent));
                        } else {
                            final PerDiem perDiem = getPerDiemService().getPerDiem(
                                    restoredLine.getPrimaryDestinationId(), perDiemExpense.getMileageDate(),
                                    travelDocument.getEffectiveDateForPerDiem(perDiemExpense));
                            setPerDiemMealsAndIncidentals(perDiemExpense, perDiem, travelDocument.getTripType(),
                                    travelDocument.getTripEnd(), true);
                        }
                    }
                    if (travelDocument.getTripEnd() != null && KfsDateUtils.isSameDay(travelDocument.getTripEnd(),
                            perDiemExpense.getMileageDate())) {
                        // set lodging to 0
                        perDiemExpense.setLodging(KualiDecimal.ZERO);
                    }
                } else {
                    // are we copying a prorated line to a non-prorated spot?

                    // then let's restore all values before copying
                    perDiemExpense = lineToCopy;
                }

                tempPerDiemExpenses.add(perDiemExpense);

            }
        }

        perDiemExpenses.clear();
        perDiemExpenses.addAll(tempPerDiemExpenses);
    }

    /**
     * If the given perDiemExpense was prorated, restores the original values
     * @param travelDocument the travel document the expense is on
     * @param perDiemExpense the per diem expense to restore
     * @return a PerDiemExpense with all values restored
     */
    protected PerDiemExpense getRestoredPerDiemForCopying(TravelDocument travelDocument,
            PerDiemExpense perDiemExpense) {
        PerDiemExpense restoredExpense = copyPerDiemExpense(perDiemExpense);
        if (travelDocument.getPrimaryDestinationId() == TemConstants.CUSTOM_PRIMARY_DESTINATION_ID && shouldProrate(
                perDiemExpense.getMileageDate(), travelDocument.getTripBegin(), travelDocument.getTripEnd())) {
            final Integer perDiemPercentage = lookupProratePercentage(perDiemExpense,
                    travelDocument.getTripType().getPerDiemCalcMethod(), travelDocument.getTripEnd());
            if (perDiemPercentage != null) {
                final KualiDecimal perDiemPercentageDecimal = new KualiDecimal((double) perDiemPercentage * 0.01);
                restoredExpense
                        .setBreakfastValue(perDiemExpense.getBreakfastValue().divide(perDiemPercentageDecimal));
                restoredExpense.setLunchValue(perDiemExpense.getLunchValue().divide(perDiemPercentageDecimal));
                restoredExpense.setDinnerValue(perDiemExpense.getDinnerValue().divide(perDiemPercentageDecimal));
                restoredExpense
                        .setIncidentalsValue(perDiemExpense.getIncidentalsValue().divide(perDiemPercentageDecimal));
            }
            perDiemExpense.setProrated(false);
        } else {
            // look up per diem
            final PerDiem perDiem = getPerDiemService().getPerDiem(perDiemExpense.getPrimaryDestinationId(),
                    perDiemExpense.getMileageDate(), travelDocument.getEffectiveDateForPerDiem(perDiemExpense));
            setPerDiemMealsAndIncidentals(restoredExpense, perDiem, travelDocument.getTripType(),
                    travelDocument.getTripEnd(), false);
        }
        return restoredExpense;
    }

    /**
     * Takes the values from the given per diem expense and copies them into a per diem
     * @param perDiemExpense the per diem expense to copy values from
     * @return a fake PerDiem record copied from those values
     */
    protected PerDiem copyIntoPerDiem(PerDiemExpense perDiemExpense) {
        PerDiem perDiem = new PerDiem();
        perDiem.setPrimaryDestinationId(perDiemExpense.getPrimaryDestinationId());
        perDiem.setBreakfast(perDiemExpense.getBreakfastValue());
        perDiem.setLunch(perDiemExpense.getLunchValue());
        perDiem.setDinner(perDiemExpense.getDinnerValue());
        perDiem.setIncidentals(perDiemExpense.getIncidentalsValue());
        return perDiem;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getDocumentsRelatedTo(org.kuali.kfs.module.tem.document.TravelDocument)
     */
    @Override
    public Map<String, List<Document>> getDocumentsRelatedTo(final TravelDocument document)
            throws WorkflowException {
        return getDocumentsRelatedTo(document.getDocumentNumber());
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getDocumentsRelatedTo(java.lang.String)
     */
    @Override
    public Map<String, List<Document>> getDocumentsRelatedTo(final String documentNumber) throws WorkflowException {
        final Map<String, List<Document>> retval = new HashMap<String, List<Document>>();

        Set<String> documentNumbers = accountingDocumentRelationshipService
                .getAllRelatedDocumentNumbers(documentNumber);
        if (!documentNumbers.isEmpty()) {
            for (String documentHeaderId : documentNumbers) {
                Document doc = documentService.getByDocumentHeaderIdSessionless(documentHeaderId);
                if (doc != null) {
                    Class<? extends Document> clazz = doc.getClass();

                    if (clazz != null) {
                        String docTypeName = getDataDictionaryService().getDocumentTypeNameByClass(clazz);

                        List<Document> docs = retval.get(docTypeName);
                        if (docs == null) {
                            docs = new ArrayList<Document>();
                        }
                        docs.add(doc);

                        retval.put(docTypeName, docs);
                    }
                }
            }
        }

        return retval;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getDocumentsRelatedTo(org.kuali.kfs.module.tem.document.TravelDocument, java.lang.String[])
     */
    @Override
    public List<Document> getDocumentsRelatedTo(final TravelDocument document, String... documentTypeList) {
        List<Document> relatedDocumentList = new ArrayList<Document>();
        Map<String, List<Document>> relatedDocumentMap;
        try {
            relatedDocumentMap = getDocumentsRelatedTo(document);
            for (String documentType : documentTypeList) {
                if (relatedDocumentMap.containsKey(documentType)) {
                    relatedDocumentList.addAll(relatedDocumentMap.get(documentType));
                }
            }
        } catch (WorkflowException ex) {
            LOG.error(ex.getMessage(), ex);
            throw new RuntimeException(ex);
        }
        return relatedDocumentList;
    }

    @Override
    public List<SpecialCircumstances> findActiveSpecialCircumstances(String documentNumber, String documentType) {
        List<SpecialCircumstances> retval = new ArrayList<SpecialCircumstances>();
        Map<String, Object> criteria = new HashMap<String, Object>();
        criteria.put(KFSPropertyConstants.ACTIVE, true);

        // add specialCircumstances with specific documentType SpecialCircumstancesQuestion
        Set<String> documentTypesToCheck = new HashSet<String>();
        documentTypesToCheck.add(documentType);
        final Set<String> parentDocTypes = getTravelService().getParentDocumentTypeNames(documentType);
        documentTypesToCheck.addAll(parentDocTypes);
        criteria.put(KFSPropertyConstants.DOCUMENT_TYPE, documentTypesToCheck);
        retval.addAll(buildSpecialCircumstances(documentNumber, criteria));

        return retval;
    }

    protected List<SpecialCircumstances> buildSpecialCircumstances(String documentNumber,
            Map<String, Object> criteria) {
        List<SpecialCircumstances> retval = new ArrayList<SpecialCircumstances>();

        Collection<SpecialCircumstancesQuestion> questions = getBusinessObjectService()
                .findMatching(SpecialCircumstancesQuestion.class, criteria);
        for (SpecialCircumstancesQuestion question : questions) {
            SpecialCircumstances spc = new SpecialCircumstances();
            spc.setDocumentNumber(documentNumber);
            spc.setQuestionId(question.getId());
            spc.setQuestion(question);
            retval.add(spc);
        }

        return retval;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#findAuthorizationDocuments(java.lang.String)
     */
    @Override
    public List<TravelAuthorizationDocument> findAuthorizationDocuments(final String travelDocumentIdentifier) {
        final List<String> ids = findAuthorizationDocumentNumbers(travelDocumentIdentifier);

        List<TravelAuthorizationDocument> resultDocumentLists = new ArrayList<TravelAuthorizationDocument>();
        //retrieve the actual documents
        try {
            if (!ids.isEmpty()) {
                for (Document document : getDocumentService()
                        .getDocumentsByListOfDocumentHeaderIds(TravelAuthorizationDocument.class, ids)) {
                    resultDocumentLists.add((TravelAuthorizationDocument) document);
                }
            }
        } catch (WorkflowException wfe) {
            LOG.error(wfe.getMessage(), wfe);
        }
        return resultDocumentLists;
    }

    /**
     * Gets the document numbers from the TravelDocumentDao for the given trip id
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#findAuthorizationDocumentNumbers(java.lang.String)
     */
    @Override
    public List<String> findAuthorizationDocumentNumbers(final String travelDocumentIdentifier) {
        final List<String> ids = getTravelDocumentDao().findDocumentNumbers(TravelAuthorizationDocument.class,
                travelDocumentIdentifier);
        return ids;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#findReimbursementDocuments(java.lang.String)
     */
    @Override
    public List<TravelReimbursementDocument> findReimbursementDocuments(final String travelDocumentIdentifier) {
        final List<String> ids = getTravelDocumentDao().findDocumentNumbers(TravelReimbursementDocument.class,
                travelDocumentIdentifier);

        List<TravelReimbursementDocument> resultDocumentLists = new ArrayList<TravelReimbursementDocument>();
        // retrieve the actual documents
        try {
            if (!ids.isEmpty()) {
                for (Document document : getDocumentService()
                        .getDocumentsByListOfDocumentHeaderIds(TravelReimbursementDocument.class, ids)) {
                    resultDocumentLists.add((TravelReimbursementDocument) document);
                }
            }
        } catch (WorkflowException wfe) {
            throw new RuntimeException(wfe);
        }
        return resultDocumentLists;
    }

    /**
     *
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#addAdHocFYIRecipient(org.kuali.rice.kns.document.Document)
     */
    @Override
    public void addAdHocFYIRecipient(final Document document) {
        addAdHocFYIRecipient(document,
                document.getDocumentHeader().getWorkflowDocument().getInitiatorPrincipalId());
    }

    /**
     *
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#addAdHocFYIRecipient(org.kuali.rice.kns.document.Document, java.lang.String)
     */
    @Override
    public void addAdHocFYIRecipient(final Document document, String initiatorUserId) {
        addAdHocRecipient(document, initiatorUserId, KewApiConstants.ACTION_REQUEST_FYI_REQ);
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#addAdHocRecipient(Document, String, String)
     */
    @Override
    public void addAdHocRecipient(Document document, String initiatorUserId, String actionRequested) {
        List<AdHocRoutePerson> adHocRoutePersons = document.getAdHocRoutePersons();
        List<String> adHocRoutePersonIds = new ArrayList<String>();
        if (!adHocRoutePersons.isEmpty()) {
            for (AdHocRoutePerson ahrp : adHocRoutePersons) {
                adHocRoutePersonIds.add(ahrp.getId());
            }
        }

        // Add adhoc for initiator
        if (!adHocRoutePersonIds.contains(initiatorUserId)) {
            if (initiatorUserId != null) {
                final Person finSysUser = SpringContext.getBean(PersonService.class).getPerson(initiatorUserId);
                if (finSysUser != null) {
                    final AdHocRoutePerson recipient = buildAdHocRecipient(finSysUser.getPrincipalName(),
                            actionRequested);
                    final DocumentAuthorizer documentAuthorizer = SpringContext.getBean(DocumentHelperService.class)
                            .getDocumentAuthorizer(document);
                    if (documentAuthorizer.canReceiveAdHoc(document, finSysUser, actionRequested)) {
                        adHocRoutePersons.add(recipient);
                    }
                } else {
                    LOG.warn("finSysUser is null.");
                }
            } else {
                LOG.warn("initiatorUserId is null.");
            }
        }
    }

    /**
     * This method builds the AdHoc Route Person
     *
     * @param userId
     * @return
     */
    protected AdHocRoutePerson buildAdHocRecipient(String userId, String actionRequested) {
        AdHocRoutePerson adHocRoutePerson = new AdHocRoutePerson();
        adHocRoutePerson.setActionRequested(actionRequested);
        adHocRoutePerson.setId(userId);
        return adHocRoutePerson;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#calculateDailyTotals(java.util.List)
     */
    @Override
    public List<Map<String, KualiDecimal>> calculateDailyTotals(List<PerDiemExpense> perDiemExpenses) {
        List<Map<String, KualiDecimal>> tripTotals = new ArrayList<Map<String, KualiDecimal>>();

        for (PerDiemExpense perDiemExpense : perDiemExpenses) {
            Map<String, KualiDecimal> dailyTotal = calculateDailyTotal(perDiemExpense);
            tripTotals.add(dailyTotal);
        }

        return tripTotals;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#calculateDailyTotal(org.kuali.kfs.module.tem.businessobject.PerDiemExpense,
     *      boolean)
     */
    @Override
    public Map<String, KualiDecimal> calculateDailyTotal(PerDiemExpense perDiemExpense) {
        Map<String, KualiDecimal> dailyTotals = new HashMap<String, KualiDecimal>();

        dailyTotals.put(TemConstants.MILEAGE_TOTAL_ATTRIBUTE, perDiemExpense.getMileageTotal());
        dailyTotals.put(TemConstants.LODGING_TOTAL_ATTRIBUTE, perDiemExpense.getLodgingTotal());
        dailyTotals.put(TemConstants.MEALS_AND_INC_TOTAL_ATTRIBUTE, perDiemExpense.getMealsAndIncidentals());
        dailyTotals.put(TemConstants.DAILY_TOTAL, perDiemExpense.getDailyTotal());

        return dailyTotals;
    }

    @Override
    public void routeToFiscalOfficer(final TravelDocument document, final String noteText)
            throws WorkflowException, Exception {
        // Below used as a place holder to allow code to specify actionForward to return if not a 'success question'
        final Note newNote = getDocumentService().createNoteFromDocument(document, noteText);
        document.addNote(newNote);
        getNoteService().save(newNote);

        final WorkflowDocument workflowDocument = document.getDocumentHeader().getWorkflowDocument();
        workflowDocument.returnToPreviousNode(noteText, KFSConstants.RouteLevelNames.ACCOUNT);

        final String messagePattern = configurationService
                .getPropertyValueAsString(TemKeyConstants.MESSAGE_DOCUMENT_TEM_RETURNED_TO_FISCAL_OFFICER);
        final String annotation = MessageFormat.format(messagePattern,
                GlobalVariables.getUserSession().getPerson().getPrincipalName());

        workflowDocument.adHocToPrincipal(ActionRequestType.FYI, KFSConstants.RouteLevelNames.ACCOUNT, annotation,
                workflowDocument.getInitiatorPrincipalId(), TemConstants.INITIATOR_RESPONSIBILITY, true);

        document.refreshReferenceObject(KFSPropertyConstants.DOCUMENT_HEADER);

        document.getFinancialSystemDocumentHeader()
                .updateAndSaveAppDocStatus(TemConstants.TravelStatusCodeKeys.AWAIT_FISCAL);
    }

    /**
     *
     * This method calculates the prorate percentage value based on perDiemCalcMethod (P/Q)
     * @param expense
     * @param perDiemCalcMethod
     * @return
     */
    @Override
    public Integer calculateProratePercentage(PerDiemExpense perDiemExpense, String perDiemCalcMethod,
            Timestamp tripEnd) {
        Integer perDiemPercent = 100;

        if (perDiemExpense.isProrated()) {
            perDiemPercent = lookupProratePercentage(perDiemExpense, perDiemCalcMethod, tripEnd);
        }
        return perDiemPercent;
    }

    /**
     * Looks up the prorate percentage, even if the per diem doesn't think it's prorated
     * @param perDiemExpense the per diem expense to find a percentage for (if the quarterly method is used)
     * @param perDiemCalcMethod the per diem calculation method
     * @param tripEnd the last day of the trip (used for the quarterly method)
     * @return a prorate percentage, or 100 if nothing could be found
     */
    protected Integer lookupProratePercentage(PerDiemExpense perDiemExpense, String perDiemCalcMethod,
            Timestamp tripEnd) {
        if (perDiemCalcMethod != null && perDiemCalcMethod.equals(TemConstants.PERCENTAGE)) {
            try {
                final String perDiemPercentage = parameterService.getParameterValueAsString(
                        TravelAuthorizationDocument.class,
                        TravelAuthorizationParameters.FIRST_AND_LAST_DAY_PER_DIEM_PERCENTAGE, "100");
                final Integer perDiemPercent = Integer.parseInt(perDiemPercentage);
                return perDiemPercent;
            } catch (Exception e1) {
                LOG.error(
                        "Failed to process prorate percentage for FIRST_AND_LAST_DAY_PER_DIEM_PERCENTAGE parameter.",
                        e1);
            }
        } else {
            return calculatePerDiemPercentageFromTimestamp(perDiemExpense, tripEnd);
        }
        return 100;
    }

    @Override
    public Integer calculatePerDiemPercentageFromTimestamp(PerDiemExpense perDiemExpense, Timestamp tripEnd) {
        if (perDiemExpense.getMileageDate() != null) {
            try {
                Collection<String> quarterTimes = parameterService.getParameterValuesAsString(
                        TemParameterConstants.TEM_DOCUMENT.class, TravelParameters.QUARTER_DAY_TIME_TABLE);

                // Take date and compare to the quadrant specified.
                Calendar prorateDate = new GregorianCalendar();
                prorateDate.setTime(perDiemExpense.getMileageDate());

                int quadrantIndex = 4;
                for (String qT : quarterTimes) {
                    String[] indexTime = qT.split("=");
                    String[] hourMinute = indexTime[1].split(":");

                    Calendar qtCal = new GregorianCalendar();
                    qtCal.setTime(perDiemExpense.getMileageDate());
                    qtCal.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hourMinute[0]));
                    qtCal.set(Calendar.MINUTE, Integer.parseInt(hourMinute[1]));

                    if (prorateDate.equals(qtCal) || prorateDate.before(qtCal)) {
                        quadrantIndex = Integer.parseInt(indexTime[0]);
                        break;
                    }
                }

                // Prorate on trip begin. (12:01 AM arrival = 100%, 11:59 PM arrival = 25%)
                if (tripEnd != null && !KfsDateUtils.isSameDay(prorateDate.getTime(), tripEnd)) {
                    return 100 - ((quadrantIndex - 1) * TemConstants.QUADRANT_PERCENT_VALUE);
                } else { // Prorate on trip end. (12:01 AM departure = 25%, 11:59 PM arrival = 100%).
                    return quadrantIndex * TemConstants.QUADRANT_PERCENT_VALUE;
                }
            } catch (IllegalArgumentException e2) {
                LOG.error("IllegalArgumentException.", e2);
            }
        }

        return 100;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#transferPerDiemMileage(org.kuali.kfs.module.tem.businessobject.PerDiemMileage)
     */
    @Override
    public PerDiemExpense copyPerDiemExpense(PerDiemExpense perDiemExpense) {
        final PerDiemExpense retval = new PerDiemExpense();
        retval.setDocumentNumber(perDiemExpense.getDocumentNumber());

        retval.setCountryState(perDiemExpense.getCountryState());
        retval.setCounty(perDiemExpense.getCounty());
        retval.setPrimaryDestination(perDiemExpense.getPrimaryDestination());
        retval.setMileageDate(perDiemExpense.getMileageDate());
        retval.setMiles(perDiemExpense.getMiles());
        retval.setMileageRateExpenseTypeCode(perDiemExpense.getMileageRateExpenseTypeCode());
        retval.setAccommodationTypeCode(perDiemExpense.getAccommodationTypeCode());
        retval.setAccommodationName(perDiemExpense.getAccommodationName());
        retval.setAccommodationPhoneNum(perDiemExpense.getAccommodationPhoneNum());
        retval.setAccommodationAddress(perDiemExpense.getAccommodationAddress());
        retval.setPrimaryDestinationId(perDiemExpense.getPrimaryDestinationId());

        if (retval.getMiles() == null) {
            retval.setMiles(0);
        }

        if (perDiemExpense.getLodging() == null || perDiemExpense.getLodging().isNegative()) {
            retval.setLodging(KualiDecimal.ZERO);
        } else {
            retval.setLodging(perDiemExpense.getLodging());
        }

        retval.setPersonal(perDiemExpense.getPersonal());
        retval.setBreakfast(perDiemExpense.getBreakfast());
        retval.setLunch(perDiemExpense.getLunch());
        retval.setDinner(perDiemExpense.getDinner());

        retval.setBreakfastValue(perDiemExpense.getBreakfastValue());
        retval.setLunchValue(perDiemExpense.getLunchValue());
        retval.setDinnerValue(perDiemExpense.getDinnerValue());
        retval.setIncidentalsValue(perDiemExpense.getIncidentalsValue());

        LOG.debug("estimated meals and incidentals " + retval.getMealsAndIncidentals());

        return retval;
    }

    @Override
    /**
     * Calculates Mileage and returns total mileage amount
     * @param ActualExpense actualExpense
     */
    public KualiDecimal calculateMileage(ActualExpense actualExpense) {
        KualiDecimal mileageTotal = KualiDecimal.ZERO;
        if (ObjectUtils.isNotNull(actualExpense.getExpenseTypeCode()) && actualExpense.isMileage()) {
            mileageTotal = actualExpense.getMileageTotal();
        }
        return mileageTotal;
    }

    protected ActualExpense getParentActualExpense(final List<ActualExpense> actualExpenses, Long expenseId) {
        if (ObjectUtils.isNotNull(actualExpenses) && ObjectUtils.isNotNull(expenseId)) {

            for (final ActualExpense actualExpense : actualExpenses) {

                if (actualExpense.getId().equals(expenseId)) {
                    return actualExpense;
                }

            }
        }

        return null;
    }

    /**
     *
     */
    @Override
    public void handleNewActualExpense(final ActualExpense newActualExpenseLine) {
        if (newActualExpenseLine.getExpenseAmount() != null) {
            final BigDecimal rate = newActualExpenseLine.getCurrencyRate();
            final KualiDecimal amount = newActualExpenseLine.getExpenseAmount();

            newActualExpenseLine.setConvertedAmount(new KualiDecimal(amount.bigDecimalValue().multiply(rate)));
            LOG.debug("Set converted amount for " + newActualExpenseLine + " to "
                    + newActualExpenseLine.getConvertedAmount());

            if (isHostedMeal(newActualExpenseLine)) {
                KNSGlobalVariables.getMessageList().add(TemKeyConstants.MESSAGE_HOSTED_MEAL_ADDED,
                        new SimpleDateFormat("MM/dd/yyyy").format(newActualExpenseLine.getExpenseDate()),
                        newActualExpenseLine.getExpenseTypeObjectCode().getExpenseType().getName());
                newActualExpenseLine.setNonReimbursable(true);
            }
        }
    }

    /**
     * Determines if an object with an expense type is that of a "hosted" meal. In TEM a hosted meal is a meal that has been
     * provided by a hosting institution and cannot be taken as a reimbursement. Uses the HOSTED_MEAL_EXPENSE_TYPES system parameter
     * to check the expense type against
     *
     * @param havingExpenseType has an expense type to check for meal hosting
     * @return true if the expense is a hosted meal or not
     */
    @Override
    public boolean isHostedMeal(final ExpenseTypeAware havingExpenseType) {
        if (ObjectUtils.isNull(havingExpenseType) || StringUtils.isBlank(havingExpenseType.getExpenseTypeCode())) {
            return false;
        }

        if (havingExpenseType instanceof PersistableBusinessObject) {
            ((PersistableBusinessObject) havingExpenseType)
                    .refreshReferenceObject(TemPropertyConstants.EXPENSE_TYPE);
        }
        if (ObjectUtils.isNull(havingExpenseType.getExpenseType())) {
            return false;
        }
        return havingExpenseType.getExpenseType().isHosted();
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#isTravelManager(org.kuali.rice.kim.bo.Person)
     */
    @Override
    public boolean isTravelManager(final Person user) {
        return getTemRoleService().isTravelManager(user);
    }

    /**
     * Digs up a message from the {@link ConfigurationService} by key
     */
    @Override
    public String getMessageFrom(final String messageType, String... args) {
        String strTemp = getConfigurationService().getPropertyValueAsString(messageType);
        for (int i = 0; i < args.length; i++) {
            strTemp = strTemp.replaceAll("\\{" + i + "\\}", args[i]);
        }
        return strTemp;
    }

    /**
     * is this document in an open for reimbursement workflow state?
     *
     * @param reqForm
     * @return
     */
    @Override
    public boolean isOpen(TravelDocument document) {
        return document.getAppDocStatus().equals(TemConstants.TravelAuthorizationStatusCodeKeys.OPEN_REIMB);
    }

    /**
     * is this document in a final workflow state.
     *
     * @param reqForm
     * @return
     */
    @Override
    public boolean isFinal(TravelDocument document) {
        return document.getDocumentHeader().getWorkflowDocument().isFinal();
    }

    /**
     *
     * @param document
     * @return
     */
    @Override
    public boolean isTravelAuthorizationProcessed(TravelAuthorizationDocument document) {
        return isFinal(document) || isProcessed(document);
    }

    /**
     *
     * @param document
     * @return
     */
    @Override
    public boolean isTravelAuthorizationOpened(TravelAuthorizationDocument document) {
        return isTravelAuthorizationProcessed(document) && isOpen(document);
    }

    /**
     * is this document in a processed workflow state?
     *
     * @param reqForm
     * @return
     */
    @Override
    public boolean isProcessed(TravelDocument document) {
        return document.getDocumentHeader().getWorkflowDocument().isProcessed();
    }

    @Override
    public KualiDecimal getAmountDueFromInvoice(String documentNumber, KualiDecimal requestedAmount) {
        try {
            AccountsReceivableCustomerInvoice doc = (AccountsReceivableCustomerInvoice) documentService
                    .getByDocumentHeaderId(documentNumber);
            if (doc != null) {
                return doc.getOpenAmount();
            }
        } catch (WorkflowException we) {
            throw new RuntimeException(we);
        }

        return requestedAmount;
    }

    /**
     * Find the current travel authorization.  This includes any amendments.
     *
     * @param trDocument
     * @return
     * @throws WorkflowException
     */
    @Override
    public TravelAuthorizationDocument findCurrentTravelAuthorization(TravelDocument document) {

        TravelAuthorizationDocument travelDocument = null;

        try {
            final Map<String, List<Document>> relatedDocuments = getDocumentsRelatedTo(document);
            List<Document> taDocs = relatedDocuments.get(TravelDocTypes.TRAVEL_AUTHORIZATION_DOCUMENT);
            List<Document> taaDocs = relatedDocuments.get(TravelDocTypes.TRAVEL_AUTHORIZATION_AMEND_DOCUMENT);
            List<Document> tacDocs = relatedDocuments.get(TravelDocTypes.TRAVEL_AUTHORIZATION_CLOSE_DOCUMENT);

            //If TAC exists, it will always be the most current travel auth doc
            if (tacDocs != null && !tacDocs.isEmpty()) {
                travelDocument = (TravelAuthorizationDocument) tacDocs.get(0);
            }
            //find the TAA with the correct status
            else if (taaDocs != null && !taaDocs.isEmpty()) {
                for (Document tempDocument : taaDocs) {
                    //Find the doc that is the open to perform actions against.
                    if (isTravelAuthorizationOpened((TravelAuthorizationDocument) tempDocument)) {
                        travelDocument = (TravelAuthorizationDocument) tempDocument;
                    }
                }
            }
            //return TA doc if no amendments exist
            if (travelDocument == null) {
                //if the taDocs is null, initialize an empty list
                taDocs = taDocs == null ? new ArrayList<Document>() : taDocs;

                if (taDocs.isEmpty()) {
                    //this should find the TA document for sure
                    final List<TravelAuthorizationDocument> tempTaDocs = findAuthorizationDocuments(
                            document.getTravelDocumentIdentifier());
                    if (!tempTaDocs.isEmpty()) {
                        travelDocument = tempTaDocs.get(0);
                    }
                } else {
                    travelDocument = (TravelAuthorizationDocument) taDocs.get(0);
                }
            }
        } catch (WorkflowException we) {
            final String docNum = (document != null && !StringUtils.isBlank(document.getDocumentNumber()))
                    ? document.getDocumentNumber()
                    : "???";
            throw new RuntimeException("Could not find documents related to document #" + docNum);
        }
        return travelDocument;
    }

    /**
     * Find the root document for creating a travel reimbursement from a previous document.
     *
     * @param trDocument
     * @return
     * @throws WorkflowException
     */
    @Override
    public TravelDocument findRootForTravelReimbursement(String travelDocumentIdentifier) {

        TravelDocument rootTravelDocument = null;

        try {
            //look for a current authorization first

            //use the travelDocumentIdentifier to find any saved authorization
            final Collection<TravelAuthorizationDocument> tempTaDocs = getTravelAuthorizationService()
                    .find(travelDocumentIdentifier);

            if (!tempTaDocs.isEmpty()) {
                TravelAuthorizationDocument taDoc = null;
                for (TravelAuthorizationDocument tempTaDoc : tempTaDocs) {
                    taDoc = tempTaDoc;
                    break;
                }

                //find the current travel authorization
                rootTravelDocument = findCurrentTravelAuthorization(taDoc);
            }

            //no authorizations exist so the root should be a reimbursement
            else {
                final List<TravelReimbursementDocument> tempTrDocs = findReimbursementDocuments(
                        travelDocumentIdentifier);
                //did not find any reimbursements either
                if (tempTrDocs.isEmpty()) {
                    LOG.debug("Did not find any authorizations or reimbursements for travelDocumentIndentifier: "
                            + travelDocumentIdentifier);
                    return null;
                }

                //if there is only one document then that is the root
                if (tempTrDocs.size() == 1) {
                    rootTravelDocument = tempTrDocs.get(0);
                } else {
                    //the root document can be found using any document in the list; just use the first one
                    String rootDocumentNumber = getAccountingDocumentRelationshipService()
                            .getRootDocumentNumber(tempTrDocs.get(0).getDocumentNumber());
                    TravelDocument tempDoc = (TravelDocument) documentService
                            .getByDocumentHeaderIdSessionless(rootDocumentNumber);

                    rootTravelDocument = tempDoc;
                }
            }
        } catch (WorkflowException we) {
            throw new RuntimeException(
                    "Could not find authorization or reimbursement documents related to trip id #"
                            + travelDocumentIdentifier);
        }

        return rootTravelDocument;
    }

    @Override
    public KualiDecimal getTotalCumulativeReimbursements(TravelDocument document) {
        KualiDecimal trTotal = KualiDecimal.ZERO;

        List<Document> relatedTravelReimbursementDocuments = getDocumentsRelatedTo(document,
                TravelDocTypes.TRAVEL_REIMBURSEMENT_DOCUMENT);
        for (Document trDoc : relatedTravelReimbursementDocuments) {
            final TravelReimbursementDocument tr = (TravelReimbursementDocument) trDoc;
            if (!KFSConstants.DocumentStatusCodes.CANCELLED
                    .equals(tr.getFinancialSystemDocumentHeader().getFinancialDocumentStatusCode())
                    && !KFSConstants.DocumentStatusCodes.DISAPPROVED
                            .equals(tr.getFinancialSystemDocumentHeader().getFinancialDocumentStatusCode())) {
                List<AccountingLine> lines = tr.getSourceAccountingLines();
                for (AccountingLine line : lines) {
                    trTotal = trTotal.add(line.getAmount());
                }
            }
        }

        if (document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName()
                .equals(TravelDocTypes.TRAVEL_REIMBURSEMENT_DOCUMENT)) {
            List<AccountingLine> lines = document.getSourceAccountingLines();
            for (AccountingLine line : lines) {
                trTotal = trTotal.add(line.getAmount());
            }
        }

        return trTotal;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getTotalAuthorizedEncumbrance(org.kuali.kfs.module.tem.document.TravelDocument)
     */
    @Override
    public KualiDecimal getTotalAuthorizedEncumbrance(TravelDocument document) {
        KualiDecimal taTotal = KualiDecimal.ZERO;
        TravelAuthorizationDocument taDoc = null;
        taDoc = findCurrentTravelAuthorization(document);
        if (taDoc != null) {
            List<AccountingLine> lines = taDoc.getSourceAccountingLines();
            for (AccountingLine line : lines) {
                taTotal = taTotal.add(line.getAmount());
            }
        }
        return taTotal;

    }

    /**
     * Determines if the user is a fiscal officer on {@link Account} instances tied to the {@link TravelAuthorizationDocument}
     * instance
     *
     * @param authorization to check for fiscal officer status on
     * @param principalId is a Person that might be a fiscal officer on account
     * @return if the <code>user</code> is a fiscal officer on accounts tied to the {@link TravelAuthorizationDocument}
     */
    @Override
    public boolean isResponsibleForAccountsOn(final TravelDocument document, String principalId) {
        final List<String> accounts = findAccountsResponsibleFor(document.getSourceAccountingLines(), principalId);
        return (accounts != null && accounts.size() > 0);
    }

    /**
     * Looks up accounts from {@link List} of {@link SourceAccountingLine} instances to determine if {@link Person} <code>user</code>
     * is a fiscal officer on any of those
     *
     * @param lines or {@link List} of {@link SourceAccountingLine} instances
     * @param principalId is a Person that might be a fiscal officer on accounts in <code>lines</code>
     * @return a {@link List} of account numbers the {@link Person} is a fiscal officer on
     */
    protected List<String> findAccountsResponsibleFor(final List<SourceAccountingLine> lines, String principalId) {
        final Set<String> accountList = new HashSet<String>();
        for (AccountingLine line : lines) {
            line.refreshReferenceObject(KFSPropertyConstants.ACCOUNT);
            if (line != null && !ObjectUtils.isNull(line.getAccount())) {
                Person accountFiscalOfficerUser = line.getAccount().getAccountFiscalOfficerUser();
                if (accountFiscalOfficerUser != null
                        && accountFiscalOfficerUser.getPrincipalId().equals(principalId)) {
                    accountList.add(line.getAccountNumber());
                }
            }
        }
        return new ArrayList<String>(accountList);
    }

    /**
     * This method checks to see if the type code is for a non-employee
     *
     * @param travelerTypeCode
     */
    @Override
    public boolean checkNonEmployeeTravelerTypeCode(String travelerTypeCode) {
        boolean foundCode = false;
        if (getParameterService().getParameterValuesAsString(TemParameterConstants.TEM_DOCUMENT.class,
                TravelParameters.NON_EMPLOYEE_TRAVELER_TYPE_CODES).contains(travelerTypeCode)) {
            foundCode = true;
        }
        return foundCode;
    }

    /**
     *
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getAllStates(java.lang.String)
     */
    @Override
    public String getAllStates(final String countryCode) {

        final List<State> codes = getStateService().findAllStatesInCountry(countryCode);

        final StringBuffer sb = new StringBuffer();
        sb.append(";");
        for (final State state : codes) {
            if (state.isActive()) {
                sb.append(state.getCode()).append(";");
            }
        }

        return sb.toString();
    }

    /**
     *
     * This method imports the file and convert it to a list of objects (of the class specified in the parameter)
     *
     * TODO: re-evaluate KUALITEM-954 in regards to defaultValues and attributeMaxLength. Validation should not happen at parsing (these param are only used by importAttendees in TravelEntertainmentAction).
     *
     * @param formFile
     * @param c
     * @param attributeNames
     * @param tabErrorKey
     * @return
     */
    @Override
    public <T> List<T> importFile(final String fileContents, final Class<T> c, final String[] attributeNames,
            final Map<String, List<String>> defaultValues, final Integer[] attributeMaxLength,
            final String tabErrorKey) {
        if (attributeMaxLength != null && attributeNames.length != attributeMaxLength.length) {
            throw new UploadParserException(
                    "Invalid parser configuration, the number of attribute names and attribute max length should be the same");
        }

        return importFile(fileContents, c, attributeNames, defaultValues, attributeMaxLength, tabErrorKey,
                getDefaultAcceptableFileExtensions());
    }

    /**
     *
     * This method imports the file and convert it to a list of objects (of the class specified in the parameter)
     * @param formFile
     * @param c
     * @param attributeNames
     * @param tabErrorKey
     * @param fileExtensions
     * @return
     */
    public <T> List<T> importFile(final String fileContents, Class<T> c, String[] attributeNames,
            Map<String, List<String>> defaultValues, Integer[] attributeMaxLength, String tabErrorKey,
            List<String> fileExtensions) {
        final List<T> importedObjects = new ArrayList<T>();

        // parse file line by line
        Integer lineNo = 0;
        boolean failed = false;
        for (final String line : fileContents.split("\n")) {
            lineNo++;
            try {
                final T o = parseLine(line, c, attributeNames, defaultValues, attributeMaxLength, lineNo,
                        tabErrorKey);
                importedObjects.add(o);
            } catch (UploadParserException e) {
                // continue to parse the rest of the lines after the current line fails
                // error messages are already dealt with inside parseFile, so no need to do anything here
                failed = true;
            }
        }

        if (failed) {
            throw new UploadParserException("errors in parsing lines in file ", ERROR_UPLOADPARSER_LINE);
        }

        return importedObjects;
    }

    /**
     *
     * This method parses a CSV line
     * @param line
     * @param c
     * @param attributeNames
     * @param lineNo
     * @param tabErrorKey
     * @return
     */
    protected <T> T parseLine(String line, Class<T> c, String[] attributeNames,
            Map<String, List<String>> defaultValues, Integer[] attributeMaxLength, Integer lineNo,
            String tabErrorKey) {
        final Map<String, String> objectMap = retrieveObjectAttributes(line, attributeNames, defaultValues,
                attributeMaxLength, lineNo, tabErrorKey);
        final T obj = genObjectWithRetrievedAttributes(objectMap, c, lineNo, tabErrorKey);
        ((PersistableBusinessObject) obj).refresh();
        return obj;
    }

    /**
     *
     * This method generates an object instance and populates it with the specified attribute map.
     * @param objectMap
     * @param c
     * @param lineNo
     * @param tabErrorKey
     * @return
     */
    protected <T> T genObjectWithRetrievedAttributes(final Map<String, String> objectMap, final Class<T> c,
            final Integer lineNo, final String tabErrorKey) {
        T object;
        try {
            object = c.newInstance();
        } catch (Exception e) {
            throw new InfrastructureException("unable to complete line population.", e);
        }

        boolean failed = false;
        for (final Map.Entry<String, String> entry : objectMap.entrySet()) {
            try {
                try {
                    ObjectUtils.setObjectProperty(object, entry.getKey(), entry.getValue());
                } catch (FormatException e) {
                    String[] errorParams = { entry.getValue(), entry.getKey(), "" + lineNo };
                    throw new UploadParserException(
                            "invalid numeric property value: " + entry.getKey() + " = " + entry.getValue()
                                    + " (line " + lineNo + ")",
                            ERROR_UPLOADPARSER_INVALID_NUMERIC_VALUE, errorParams);
                }
            } catch (UploadParserException e) {
                // continue to parse the rest of the properties after the current property fails
                GlobalVariables.getMessageMap().putError(tabErrorKey, e.getErrorKey(), e.getErrorParameters());
                failed = true;
            } catch (NoSuchMethodException nsme) {
                throw new RuntimeException("Could not set property while parsing group travelers csv", nsme);
            } catch (InvocationTargetException ite) {
                throw new RuntimeException("Could not set property while parsing group travelers csv", ite);
            } catch (IllegalAccessException iae) {
                throw new RuntimeException("Could not set property while parsing group travelers csv", iae);
            }
        }

        if (failed) {
            throw new UploadParserException("empty or invalid properties in line " + lineNo + ")",
                    ERROR_UPLOADPARSER_PROPERTY, "" + lineNo);
        }
        return object;
    }

    /**
     *
     * This method retrieves the attributes as key-value string pairs into a map.
     * @param line
     * @param attributeNames
     * @param lineNo
     * @param tabErrorKey
     * @return
     */
    protected Map<String, String> retrieveObjectAttributes(String line, String[] attributeNames,
            Map<String, List<String>> defaultValues, Integer[] attributeMaxLength, Integer lineNo,
            String tabErrorKey) {
        String[] attributeValues = StringUtils.splitPreserveAllTokens(line, ',');
        if (attributeNames.length != attributeValues.length) {
            String[] errorParams = { "" + attributeNames.length, "" + attributeValues.length, "" + lineNo };
            GlobalVariables.getMessageMap().putError(tabErrorKey, ERROR_UPLOADPARSER_WRONG_PROPERTY_NUMBER,
                    errorParams);
            throw new UploadParserException(
                    "wrong number of properties: " + attributeValues.length + " exist, " + attributeNames.length
                            + " expected (line " + lineNo + ")",
                    ERROR_UPLOADPARSER_WRONG_PROPERTY_NUMBER, errorParams);
        }

        for (int i = 0; i < attributeNames.length; i++) {
            if (defaultValues != null && defaultValues.get(attributeNames[i]) != null) {
                List<String> defaultValue = defaultValues.get(attributeNames[i]);
                boolean found = false;
                for (String value : defaultValue) {
                    if (attributeValues[i].equalsIgnoreCase(value)) {
                        found = true;
                    }
                }
                if (!found) {
                    GlobalVariables.getMessageMap().putWarning(tabErrorKey, MESSAGE_UPLOADPARSER_INVALID_VALUE,
                            attributeNames[i], attributeValues[i], (" " + lineNo));
                    throw new UploadParserException(
                            "Invalid value " + attributeValues[i] + " exist, " + "in line (" + lineNo + ")",
                            ERROR_UPLOADPARSER_WRONG_PROPERTY_NUMBER);
                }
            }

            if (attributeMaxLength != null) {
                if (attributeValues[i] != null && attributeValues[i].length() > attributeMaxLength[i]) {
                    attributeValues[i] = attributeValues[i].substring(0, attributeMaxLength[i]);
                    String[] errorParams = { "" + attributeNames[i], "" + attributeMaxLength[i], "" + lineNo };
                    GlobalVariables.getMessageMap().putWarning(tabErrorKey,
                            MESSAGE_UPLOADPARSER_EXCEEDED_MAX_LENGTH, errorParams);
                }
            }
        }

        Map<String, String> objectMap = new HashMap<String, String>();
        for (int i = 0; i < attributeNames.length; i++) {
            objectMap.put(attributeNames[i], attributeValues[i]);
        }

        return objectMap;
    }

    /**
     * Parses a header into some usable form that can be used to parse records from the
     * CSV
     *
     * @param csvHeader is an array of columns for a csv record
     * @return a {@link Map} keyed by field names to their column numbers
     */
    protected Map<String, List<Integer>> parseHeader(final String[] csvHeader) {
        final Map<String, List<Integer>> retval = new HashMap<String, List<Integer>>();

        for (Integer i = 0; i < csvHeader.length; i++) {

            if (StringUtils.isBlank(csvHeader[i].trim())) {
                final String formattedName = nextHeader(csvHeader, i);
                final Integer start = i;
                final Integer end = csvHeader.length > i ? nextBlankHeader(csvHeader, i) : i;

                final List<Integer> indexes = new ArrayList<Integer>();

                for (Integer y = start; y < end; y++) {
                    indexes.add(y);
                }
                retval.put(formattedName, indexes);
            } else {
                final String formattedName = toCamelCase(csvHeader[i]);

                if (StringUtils.isNotBlank(formattedName)) {
                    retval.put(formattedName, Arrays.asList(new Integer[] { i }));
                }
            }
        }
        return retval;
    }

    protected String nextHeader(final String[] headers, final int start) {
        for (int i = start + 1; i < headers.length; i++) {
            if (StringUtils.isNotBlank(headers[i])) {
                return toCamelCase(headers[i]);
            }
        }
        return "";
    }

    protected Integer nextBlankHeader(final String[] headers, final int start) {
        for (int i = start + 1; i < headers.length; i++) {
            if (StringUtils.isBlank(headers[i])) {
                return i;
            }
        }
        return -1;
    }

    protected String toProperCase(final String s) {
        if (s == null || s.length() < 1) {
            return "";
        }

        final char[] arr = s.toLowerCase().toCharArray();
        arr[0] = Character.toUpperCase(arr[0]);

        return new String(arr);
    }

    protected String toCamelCase(final String s) {
        final StringBuffer buffer = new StringBuffer();

        final List<String> words = new LinkedList<String>(
                Arrays.asList(s.toLowerCase().trim().replace('_', ' ').split(" ")));
        buffer.append(words.remove(0));

        for (final String word : words) {
            buffer.append(toProperCase(word));
        }
        return buffer.toString();
    }

    /**
     *
     */
    @Override
    public List<GroupTraveler> importGroupTravelers(final TravelDocument document, final String csvData)
            throws Exception {
        final List<GroupTraveler> retval = new LinkedList<GroupTraveler>();
        final BufferedReader bufferedFileReader = new BufferedReader(new StringReader(csvData));
        final CSVReader csvReader = new CSVReader(bufferedFileReader);

        final List<String[]> rows;
        try {
            rows = csvReader.readAll();
        } catch (IOException ex) {
            ex.printStackTrace();
            throw new ParseException("Could not  parse CSV file data", ex);
        } finally {
            try {
                csvReader.close();
            } catch (Exception e) {
            }
        }

        final Map<String, List<Integer>> header = getGroupTravelerHeaders();

        for (final String[] row : rows) {
            final GroupTravelerCsvRecord record = createGroupTravelerCsvRecord(header, row);
            final GroupTraveler traveler = new GroupTraveler();
            traveler.setGroupTravelerEmpId(record.getGroupTravelerEmpId());
            traveler.setName(record.getName());
            traveler.setGroupTravelerType(record.getGroupTravelerType());
            retval.add(traveler);
        }

        return retval;
    }

    protected GroupTravelerCsvRecord createGroupTravelerCsvRecord(final Map<String, List<Integer>> header,
            final String[] record) throws Exception {
        return getCsvRecordFactory().newInstance(header, record);
    }

    @Override
    public boolean isUnsuccessful(TravelDocument document) {
        String status = document.getDocumentHeader().getWorkflowDocument().getStatus().getCode();
        List<String> unsuccessful = KewApiConstants.DOCUMENT_STATUS_PARENT_TYPES
                .get(KewApiConstants.DOCUMENT_STATUS_PARENT_TYPE_UNSUCCESSFUL);
        for (String tempStatus : unsuccessful) {
            if (status.equals(tempStatus)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Turns the injected List of groupTravelerHeaders into a Map where the key is the name and the value is a single element array holding the position of the column (which is assumed to be in the order the columns were injected)
     * @return a Map of columns and positions
     */
    protected Map<String, List<Integer>> getGroupTravelerHeaders() {
        Map<String, List<Integer>> headers = new HashMap<String, List<Integer>>();
        if (getGroupTravelerColumns() != null && !getGroupTravelerColumns().isEmpty()) {
            int count = 0;
            while (count < getGroupTravelerColumns().size()) {
                List<Integer> countArray = new ArrayList<Integer>(2);
                countArray.add(new Integer(count));
                final String columnName = getGroupTravelerColumns().get(count);
                headers.put(columnName, countArray);
                count += 1;
            }
        }
        return headers;
    }

    /**
     *
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copyGroupTravelers(java.util.List, java.lang.String)
     */
    @Override
    public List<GroupTraveler> copyGroupTravelers(List<GroupTraveler> groupTravelers, String documentNumber) {
        List<GroupTraveler> newGroupTravelers = new ArrayList<GroupTraveler>();
        if (groupTravelers != null) {
            for (GroupTraveler groupTraveler : groupTravelers) {
                GroupTraveler newGroupTraveler = new GroupTraveler();
                BeanUtils.copyProperties(groupTraveler, newGroupTraveler);
                newGroupTraveler.setDocumentNumber(documentNumber);
                newGroupTraveler.setVersionNumber(new Long(1));
                newGroupTraveler.setObjectId(null);
                newGroupTraveler.setId(null);
                newGroupTravelers.add(newGroupTraveler);
            }
        }
        return newGroupTravelers;
    }

    /**
     *
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copyActualExpenses(java.util.List, java.lang.String)
     */
    @Override
    public List<? extends TemExpense> copyActualExpenses(List<? extends TemExpense> actualExpenses,
            String documentNumber) {
        List<ActualExpense> newActualExpenses = new ArrayList<ActualExpense>();

        if (actualExpenses != null) {
            for (TemExpense expense : actualExpenses) {
                ActualExpense actualExpense = (ActualExpense) expense;
                ActualExpense newActualExpense = new ActualExpense();
                boolean nullCheck = false;
                if (actualExpense.getExpenseDate() == null) {
                    nullCheck = true;
                    actualExpense.setExpenseDate(new Date(0));
                }
                BeanUtils.copyProperties(actualExpense, newActualExpense);
                if (nullCheck) {
                    actualExpense.setExpenseDate(null);
                    newActualExpense.setExpenseDate(null);
                }

                List<TemExpense> newDetails = (List<TemExpense>) this
                        .copyActualExpenses(actualExpense.getExpenseDetails(), documentNumber);
                newActualExpense.setExpenseDetails(newDetails);
                newActualExpense.setDocumentNumber(documentNumber);
                newActualExpense.setVersionNumber(new Long(1));
                newActualExpense.setId(null);
                newActualExpense.setObjectId(null);
                newActualExpenses.add(newActualExpense);
            }
        }
        return newActualExpenses;
    }

    private Long getNextActualExpenseId() {
        return SpringContext.getBean(SequenceAccessorService.class)
                .getNextAvailableSequenceNumber(TemConstants.TEM_ACTUAL_EXPENSE_SEQ_NAME);
    }

    /**
     *
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copyPerDiemExpenses(java.util.List, java.lang.String)
     */
    @Override
    public List<PerDiemExpense> copyPerDiemExpenses(List<PerDiemExpense> perDiemExpenses, String documentNumber) {
        List<PerDiemExpense> newPerDiemExpenses = new ArrayList<PerDiemExpense>();
        if (perDiemExpenses != null) {
            for (PerDiemExpense expense : perDiemExpenses) {
                PerDiemExpense newExpense = new PerDiemExpense();
                BeanUtils.copyProperties(expense, newExpense);
                newExpense.setBreakfastValue(expense.getBreakfastValue());
                newExpense.setLunchValue(expense.getLunchValue());
                newExpense.setDinnerValue(expense.getDinnerValue());
                newExpense.setIncidentalsValue(expense.getIncidentalsValue());
                newExpense.setDocumentNumber(documentNumber);
                newExpense.setVersionNumber(new Long(1));
                newExpense.setObjectId(null);
                newExpense.setId(null);
                newPerDiemExpenses.add(newExpense);
            }
        }
        return newPerDiemExpenses;
    }

    /**
     *
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copyTravelAdvances(java.util.List, java.lang.String)
     */
    @Override
    public List<TravelAdvance> copyTravelAdvances(List<TravelAdvance> travelAdvances, String documentNumber) {
        List<TravelAdvance> newTravelAdvances = new ArrayList<TravelAdvance>();
        if (travelAdvances != null) {
            for (TravelAdvance travelAdvance : travelAdvances) {
                TravelAdvance newTravelAdvance = (TravelAdvance) ObjectUtils.deepCopy(travelAdvance);
                newTravelAdvance.setDocumentNumber(documentNumber);
                newTravelAdvance.setVersionNumber(new Long(1));
                newTravelAdvance.setObjectId(null);
                newTravelAdvance.setTravelDocumentIdentifier(travelAdvance.getTravelDocumentIdentifier());
                newTravelAdvances.add(newTravelAdvance);
            }
        }
        return newTravelAdvances;
    }

    /**
     *
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copySpecialCircumstances(java.util.List, java.lang.String)
     */
    @Override
    public List<SpecialCircumstances> copySpecialCircumstances(List<SpecialCircumstances> specialCircumstancesList,
            String documentNumber) {
        List<SpecialCircumstances> newSpecialCircumstancesList = new ArrayList<SpecialCircumstances>();
        if (specialCircumstancesList != null) {
            for (SpecialCircumstances specialCircumstances : specialCircumstancesList) {
                SpecialCircumstances newSpecialCircumstances = new SpecialCircumstances();
                BeanUtils.copyProperties(specialCircumstances, newSpecialCircumstances);
                newSpecialCircumstances.setDocumentNumber(documentNumber);
                newSpecialCircumstances.setVersionNumber(new Long(1));
                newSpecialCircumstances.setObjectId(null);
                newSpecialCircumstances.setId(null);
                newSpecialCircumstancesList.add(newSpecialCircumstances);
            }
        }
        return newSpecialCircumstancesList;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copyTransportationModeDetails(java.util.List, java.lang.String)
     */
    @Override
    public List<TransportationModeDetail> copyTransportationModeDetails(
            List<TransportationModeDetail> transportationModeDetails, String documentNumber) {
        List<TransportationModeDetail> newTransportationModeDetails = new ArrayList<TransportationModeDetail>();
        if (transportationModeDetails != null) {
            for (TransportationModeDetail detail : transportationModeDetails) {
                TransportationModeDetail newDetail = new TransportationModeDetail();
                BeanUtils.copyProperties(detail, newDetail);
                newDetail.setDocumentNumber(documentNumber);
                newDetail.setVersionNumber(new Long(1));
                newDetail.setObjectId(null);
                newTransportationModeDetails.add(newDetail);
            }
        }
        return newTransportationModeDetails;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#showNoTravelAuthorizationError(org.kuali.kfs.module.tem.document.TravelReimbursementDocument)
     */
    @Override
    public void showNoTravelAuthorizationError(TravelReimbursementDocument document) {
        if (document.getTripType() != null && document.getTripType().getTravelAuthorizationRequired()) {
            TravelAuthorizationDocument authorization = findCurrentTravelAuthorization(document);
            if (authorization == null) {
                GlobalVariables.getMessageMap().putError(
                        KRADPropertyConstants.DOCUMENT + "." + TemPropertyConstants.TRIP_TYPE_CODE,
                        TemKeyConstants.ERROR_TRIP_TYPE_TA_REQUIRED, document.getTripType().getName());
            }
        }
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelAuthorizationService#getAdvancesTotalFor(TravelDocument)
     */
    @Override
    public KualiDecimal getAdvancesTotalFor(TravelDocument travelDocument) {
        KualiDecimal retval = KualiDecimal.ZERO;
        if (ObjectUtils.isNull(travelDocument)) {
            return retval;
        }

        LOG.debug("Looking for travel advances for travel: " + travelDocument.getDocumentNumber());

        TravelAuthorizationDocument authorization = null;
        authorization = findCurrentTravelAuthorization(travelDocument);

        if (authorization == null) {
            return retval;
        }
        authorization.refreshReferenceObject(TemPropertyConstants.TRVL_ADV);

        if (authorization.shouldProcessAdvanceForDocument()) {
            retval = retval.add(authorization.getTravelAdvance().getTravelAdvanceRequested());
        }
        return retval;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#retrieveAddressFromLocationCode(java.lang.String)
     */
    @Override
    public String retrieveAddressFromLocationCode(String locationCode) {
        PaymentDocumentationLocation dvDocumentLocation = businessObjectService
                .findBySinglePrimaryKey(PaymentDocumentationLocation.class, locationCode);
        String address = ObjectUtils.isNotNull(dvDocumentLocation)
                ? dvDocumentLocation.getPaymentDocumentationLocationAddress()
                : "";
        return address;
    }

    @Override
    public boolean validateSourceAccountingLines(TravelDocument travelDocument, boolean addToErrorPath) {
        boolean success = true;
        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put(KRADPropertyConstants.DOCUMENT_NUMBER, travelDocument.getDocumentNumber());
        fieldValues.put(KFSPropertyConstants.FINANCIAL_DOCUMENT_LINE_TYPE_CODE,
                KFSConstants.SOURCE_ACCT_LINE_TYPE_CODE);

        List<TemSourceAccountingLine> currentLines = (List<TemSourceAccountingLine>) getBusinessObjectService()
                .findMatchingOrderBy(TemSourceAccountingLine.class, fieldValues,
                        KFSPropertyConstants.SEQUENCE_NUMBER, true);

        final boolean canUpdate = isAtTravelNode(travelDocument.getDocumentHeader().getWorkflowDocument()); // Are we at the travel node?  If so, there's a chance that accounting lines changed; if they did, that
        // was a permission granted to the travel manager so we should allow it

        for (int i = 0; i < travelDocument.getSourceAccountingLines().size(); i++) {
            AccountingLine line = (AccountingLine) travelDocument.getSourceAccountingLines().get(i);
            if (addToErrorPath) {
                GlobalVariables.getMessageMap().getErrorPath()
                        .add("document." + TemPropertyConstants.SOURCE_ACCOUNTING_LINE + "[" + i + "]");
            }
            if (StringUtils.isBlank(line.getAccountNumber())) {
                success = false;
                GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_NUMBER,
                        KFSKeyConstants.ERROR_REQUIRED, "Account Number");
            } else {
                if ((!travelDocument.getAppDocStatus().equalsIgnoreCase("Initiated"))
                        && (!travelDocument.getAppDocStatus()
                                .equalsIgnoreCase(TemConstants.TravelAuthorizationStatusCodeKeys.IN_PROCESS))
                        && (!travelDocument.getAppDocStatus().equalsIgnoreCase(
                                TemConstants.TravelAuthorizationStatusCodeKeys.CHANGE_IN_PROCESS))) {
                    if ((i < currentLines.size())
                            && (!(currentLines.get(i)).getAccountNumber().equals(line.getAccountNumber()))
                            || (i >= currentLines.size())) {
                        try {
                            if (!line.getAccount().getAccountFiscalOfficerUser().getPrincipalId()
                                    .equals(GlobalVariables.getUserSession().getPerson().getPrincipalId())
                                    && !canUpdate) {
                                GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_NUMBER,
                                        TemKeyConstants.ERROR_TA_FISCAL_OFFICER_ACCOUNT, line.getAccountNumber());
                                success = false;
                            }
                        } catch (Exception e) {
                            //do nothing, other validation will figure out this account doesn't exist
                        }
                    }
                }
            }
            if (StringUtils.isBlank(line.getChartOfAccountsCode())) {
                success = false;
                GlobalVariables.getMessageMap().putError(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE,
                        KFSKeyConstants.ERROR_REQUIRED, "Chart");
            }
            if (addToErrorPath) {
                GlobalVariables.getMessageMap().getErrorPath()
                        .remove(GlobalVariables.getMessageMap().getErrorPath().size() - 1);
            }
        }

        return success;
    }

    /**
     * This method parses out the options from the parameters table and sets boolean values for each one LODGING MILEAGE PER_DIEM
     *
     * @param perDiemCats
     */
    private boolean showPerDiem(List<String> perDiemCats, String perDiemType) {
        for (String category : perDiemCats) {
            String[] pair = category.split("=");
            if (pair[0].equalsIgnoreCase(perDiemType)) {
                return pair[1].equalsIgnoreCase(TemConstants.YES);
            }
            if (pair[0].equalsIgnoreCase(perDiemType)) {
                return pair[1].equalsIgnoreCase(TemConstants.YES);
            }
            if (pair[0].equalsIgnoreCase(perDiemType)) {
                return pair[1].equalsIgnoreCase(TemConstants.YES);
            }
        }

        return false;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getOutstandingTravelAdvanceByInvoice(java.util.Set)
     */
    @Override
    public List<TravelAdvance> getOutstandingTravelAdvanceByInvoice(Set<String> arInvoiceDocNumbers) {
        return travelDocumentDao.getOutstandingTravelAdvanceByInvoice(arInvoiceDocNumbers);
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#findLatestTaxableRamificationNotificationDate()
     */
    @Override
    public Date findLatestTaxableRamificationNotificationDate() {
        Object[] returnResult = travelDocumentDao.findLatestTaxableRamificationNotificationDate();
        Date date = null;
        try {
            date = ObjectUtils.isNotNull(returnResult[0])
                    ? dateTimeService.convertToSqlDate((Timestamp) returnResult[0])
                    : null;
        } catch (java.text.ParseException ex) {
            LOG.error("Invalid latest taxable ramification notification date " + returnResult[0]);
        }

        return date;
    }

    @Override
    public void detachImportedExpenses(TravelDocument document) {
        for (ImportedExpense importedExpense : document.getImportedExpenses()) {
            ExpenseUtils.assignExpense(importedExpense.getHistoricalTravelExpenseId(), null, null, null, false);
        }
        document.setImportedExpenses(new ArrayList<ImportedExpense>());
        document.setHistoricalTravelExpenses(new ArrayList<HistoricalTravelExpense>());
    }

    @Override
    public void attachImportedExpenses(TravelDocument document) {
        for (ImportedExpense importedExpense : document.getImportedExpenses()) {
            ExpenseUtils.assignExpense(importedExpense.getHistoricalTravelExpenseId(),
                    document.getTravelDocumentIdentifier(), document.getDocumentNumber(),
                    document.getFinancialDocumentTypeCode(), true);
        }
    }

    /**
     * Check to see if the hold new fiscal year encumbrance indicator is true
     * and the trip end date is after the current fiscal year end date to determine
     * whether or not to mark all the GLPEs as 'H' (Hold)
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#holdGLPEs(org.kuali.kfs.module.tem.document.TravelDocument)
     */
    @Override
    public boolean checkHoldGLPEs(TravelDocument document) {
        if (getParameterService().getParameterValueAsBoolean(TravelAuthorizationDocument.class,
                TemConstants.TravelAuthorizationParameters.HOLD_NEW_FISCAL_YEAR_ENCUMBRANCES_IND)) {

            java.util.Date endDate = getUniversityDateService()
                    .getLastDateOfFiscalYear(getUniversityDateService().getCurrentFiscalYear());
            if (ObjectUtils.isNotNull(document.getTripBegin()) && document.getTripBegin().after(endDate)) {
                return true;
            }

        }

        return false;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#revertOriginalDocument(org.kuali.kfs.module.tem.document.TravelDocument, java.lang.String)
     */
    @Override
    public void revertOriginalDocument(TravelDocument travelDocument, String status) {
        final DocumentAttributeIndexingQueue documentAttributeIndexingQueue = KewApiServiceLocator
                .getDocumentAttributeIndexingQueue(); // this service is not a good candidate for injection
        List<Document> relatedDocumentList = getDocumentsRelatedTo(travelDocument,
                TravelDocTypes.TRAVEL_AUTHORIZATION_DOCUMENT, TravelDocTypes.TRAVEL_AUTHORIZATION_AMEND_DOCUMENT);

        for (Document taDocument : relatedDocumentList) {
            if (taDocument.getDocumentHeader().getWorkflowDocument().getApplicationDocumentStatus()
                    .equals(TravelAuthorizationStatusCodeKeys.PEND_AMENDMENT)) {
                TravelAuthorizationDocument taDoc = (TravelAuthorizationDocument) taDocument;
                try {
                    taDoc.updateAndSaveAppDocStatus(status);
                } catch (WorkflowException ex1) {
                    // TODO Auto-generated catch block
                    ex1.printStackTrace();
                }

                try {
                    Note cancelNote = getDocumentService().createNoteFromDocument(taDoc, "Amemdment Canceled");
                    Principal systemUser = KimApiServiceLocator.getIdentityService()
                            .getPrincipalByPrincipalName(KFSConstants.SYSTEM_USER);
                    cancelNote.setAuthorUniversalIdentifier(systemUser.getPrincipalId());
                    taDoc.addNote(cancelNote);
                    getNoteService().save(cancelNote);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
                documentAttributeIndexingQueue.indexDocument(taDoc.getDocumentNumber());
            }
        }
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getDocumentType(org.kuali.kfs.module.tem.document.TravelDocument)
     */
    @Override
    public String getDocumentType(TravelDocument document) {
        String documentType = null;

        if (document != null) {
            if (document instanceof TravelAuthorizationDocument) {
                documentType = TemConstants.TravelDocTypes.TRAVEL_AUTHORIZATION_DOCUMENT;
            } else if (document instanceof TravelReimbursementDocument) {
                documentType = TemConstants.TravelDocTypes.TRAVEL_REIMBURSEMENT_DOCUMENT;
            } else if (document instanceof TravelEntertainmentDocument) {
                documentType = TemConstants.TravelDocTypes.TRAVEL_ENTERTAINMENT_DOCUMENT;
            } else if (document instanceof TravelRelocationDocument) {
                documentType = TemConstants.TravelDocTypes.TRAVEL_RELOCATION_DOCUMENT;
            }
        }

        return documentType;
    }

    /**
     * Check if workflow is at the specific node
     *
     * @param workflowDocument
     * @param nodeName
     * @return
     */
    protected boolean isAtTravelNode(WorkflowDocument workflowDocument) {
        Set<String> nodeNames = workflowDocument.getNodeNames();
        for (String nodeNamesNode : nodeNames) {
            if (TemWorkflowConstants.RouteNodeNames.AP_TRAVEL.equals(nodeNamesNode)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns all travel advances associated with the given trip id
     * @see org.kuali.kfs.module.tem.document.service.TravelReimbursementService#getTravelAdvancesForTrip(java.lang.String)
     */
    @Override
    public List<TravelAdvance> getTravelAdvancesForTrip(String travelDocumentIdentifier) {
        Map<String, String> criteria = new HashMap<String, String>();
        criteria.put(TemPropertyConstants.TRAVEL_DOCUMENT_IDENTIFIER, travelDocumentIdentifier);
        List<TravelAdvance> advances = new ArrayList<TravelAdvance>();
        final Collection<TravelAdvance> foundAdvances = getBusinessObjectService()
                .findMatchingOrderBy(TravelAdvance.class, criteria, KFSPropertyConstants.DOCUMENT_NUMBER, true);
        for (TravelAdvance foundAdvance : foundAdvances) {
            if (foundAdvance.isAtLeastPartiallyFilledIn()
                    && isDocumentApprovedOrExtracted(foundAdvance.getDocumentNumber())) {
                advances.add(foundAdvance);
            }
        }
        return advances;
    }

    /**
     * Determines if the document with the given document number has been approved or not
     * @param documentNumber the document number of the document to check
     * @return true if the document has been approved, false otherwise
     */
    protected boolean isDocumentApprovedOrExtracted(String documentNumber) {
        final FinancialSystemDocumentHeader documentHeader = getBusinessObjectService()
                .findBySinglePrimaryKey(FinancialSystemDocumentHeader.class, documentNumber);
        return KFSConstants.DocumentStatusCodes.APPROVED.equals(documentHeader.getFinancialDocumentStatusCode())
                || KFSConstants.DocumentStatusCodes.Payments.EXTRACTED
                        .equals(documentHeader.getFinancialDocumentStatusCode());
    }

    /**
     * Determines if the document with the given document number has been initiated or submitted for routing
     * @param documentNumber the document number of the document to check
     * @return true if the document has been approved, false otherwise
     */
    protected boolean isDocumentInitiatedOrEnroute(String documentNumber) {
        final FinancialSystemDocumentHeader documentHeader = getBusinessObjectService()
                .findBySinglePrimaryKey(FinancialSystemDocumentHeader.class, documentNumber);
        return KFSConstants.DocumentStatusCodes.INITIATED.equals(documentHeader.getFinancialDocumentStatusCode())
                || KFSConstants.DocumentStatusCodes.ENROUTE.equals(documentHeader.getFinancialDocumentStatusCode());
    }

    /**
     * Gets the {@link OrganizationOptions} to create a {@link AccountsReceivableDocumentHeader} for
     * {@link PaymentApplicationDocument}
     *
     * @return OrganizationOptions
     */
    @Override
    public AccountsReceivableOrganizationOptions getOrgOptions() {
        final String chartOfAccountsCode = parameterService.getParameterValueAsString(
                TravelAuthorizationDocument.class, TravelAuthorizationParameters.TRAVEL_ADVANCE_BILLING_CHART);
        final String organizationCode = parameterService.getParameterValueAsString(
                TravelAuthorizationDocument.class,
                TravelAuthorizationParameters.TRAVEL_ADVANCE_BILLING_ORGANIZATION);

        return getAccountsReceivableModuleService().getOrgOptionsIfExists(chartOfAccountsCode, organizationCode);
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#disableDuplicateExpenses(org.kuali.kfs.module.tem.document.TravelReimbursementDocument, org.kuali.kfs.module.tem.businessobject.ActualExpense)
     */
    @Override
    public void disableDuplicateExpenses(TravelDocument trDocument, ActualExpense actualExpense) {
        if (trDocument.getPerDiemExpenses() != null && !trDocument.getPerDiemExpenses().isEmpty()) { // no per diems? then let's not bother
            if (actualExpense.getExpenseDetails() != null && !actualExpense.getExpenseDetails().isEmpty()) {
                for (TemExpense detail : actualExpense.getExpenseDetails()) {
                    checkActualExpenseAgainstPerDiems(trDocument, (ActualExpense) detail);
                }
            } else {
                checkActualExpenseAgainstPerDiems(trDocument, actualExpense);
            }
        }
    }

    /**
     * Checks the given actual expense (or detail) against each of the per diems on the TR document to disable
     * @param trDocument the travel reimbursement with per diems to check against
     * @param actualExpense
     */
    protected void checkActualExpenseAgainstPerDiems(TravelDocument trDocument, ActualExpense actualExpense) {
        int i = 0;
        for (final PerDiemExpense perDiemExpense : trDocument.getPerDiemExpenses()) {
            List<DisabledPropertyMessage> messages = disableDuplicateExpenseForPerDiem(actualExpense,
                    perDiemExpense, i);
            if (messages != null && !messages.isEmpty()) {
                for (DisabledPropertyMessage message : messages) {
                    message.addToProperties(trDocument.getDisabledProperties());
                }
            }
            i += 1;
        }
    }

    /**
     * Given one actual expense and one per diem, determines if any of fields on the per diem should be disabled because the actual expense is already covering it
     * @param actualExpense the actual expense to check
     * @param perDiemExpense the per diem to check the actual expense against
     * @param otherExpenseLineCode the expense type code of the actual epxnese
     * @param perDiemCount the count of the per diems we have worked through
     * @return a List of any messages about disabled properties which occurred
     */
    protected List<DisabledPropertyMessage> disableDuplicateExpenseForPerDiem(ActualExpense actualExpense,
            PerDiemExpense perDiemExpense, int perDiemCount) {
        List<DisabledPropertyMessage> disabledPropertyMessages = new ArrayList<DisabledPropertyMessage>();

        if (actualExpense.getExpenseDate() == null) {
            return disabledPropertyMessages;
        }
        final String expenseDate = getDateTimeService().toDateString(actualExpense.getExpenseDate());
        String meal = "";
        boolean valid = true;

        if (KfsDateUtils.isSameDay(perDiemExpense.getMileageDate(), actualExpense.getExpenseDate())) {
            if (perDiemExpense.getBreakfast() && actualExpense.isBreakfast()
                    && (actualExpense.getExpenseType().isHosted()
                            || actualExpense.getExpenseType().isGroupTravel())) {
                meal = TemConstants.HostedMeals.HOSTED_BREAKFAST;
                perDiemExpense.setBreakfast(false);
                perDiemExpense.setBreakfastValue(KualiDecimal.ZERO);
                valid = false;
            } else if (perDiemExpense.getLunch() && actualExpense.isLunch()
                    && (actualExpense.getExpenseType().isHosted()
                            || actualExpense.getExpenseType().isGroupTravel())) {
                meal = TemConstants.HostedMeals.HOSTED_LUNCH;
                perDiemExpense.setLunch(false);
                perDiemExpense.setLunchValue(KualiDecimal.ZERO);
                valid = false;
            } else if (perDiemExpense.getDinner() && actualExpense.isDinner()
                    && (actualExpense.getExpenseType().isHosted()
                            || actualExpense.getExpenseType().isGroupTravel())) {
                meal = TemConstants.HostedMeals.HOSTED_DINNER;
                perDiemExpense.setDinner(false);
                perDiemExpense.setDinnerValue(KualiDecimal.ZERO);
                valid = false;
            }

            if (!valid) {
                String temp = String.format(PER_DIEM_EXPENSE_DISABLED, perDiemCount, meal);
                String message = getMessageFrom(MESSAGE_TR_MEAL_ALREADY_CLAIMED, expenseDate, meal);
                disabledPropertyMessages.add(new DisabledPropertyMessage(temp, message));
            }

            // KUALITEM-483 add in check for lodging
            if (perDiemExpense.getLodging().isGreaterThan(KualiDecimal.ZERO)
                    && !StringUtils.isBlank(actualExpense.getExpenseTypeCode())
                    && TemConstants.ExpenseTypes.LODGING.equals(actualExpense.getExpenseTypeCode())) {
                String temp = String.format(PER_DIEM_EXPENSE_DISABLED, perDiemCount,
                        TemConstants.LODGING.toLowerCase());
                String message = getMessageFrom(MESSAGE_TR_LODGING_ALREADY_CLAIMED, expenseDate);
                perDiemExpense.setLodging(KualiDecimal.ZERO);
                disabledPropertyMessages.add(new DisabledPropertyMessage(temp, message));
            }
        }
        return disabledPropertyMessages;
    }

    @Override
    public List<String> findMatchingTrips(TravelDocument travelDocument) {

        String travelDocumentIdentifier = travelDocument.getTravelDocumentIdentifier();
        Integer temProfileId = travelDocument.getTemProfileId();
        Timestamp earliestTripBeginDate = null;
        Timestamp greatestTripEndDate = null;

        List<TravelReimbursementDocument> documents = findReimbursementDocuments(travelDocumentIdentifier);
        for (TravelReimbursementDocument document : documents) {
            Timestamp tripBegin = document.getTripBegin();
            Timestamp tripEnd = document.getTripEnd();
            if (ObjectUtils.isNull(earliestTripBeginDate) && ObjectUtils.isNull(greatestTripEndDate)) {
                earliestTripBeginDate = tripBegin;
                greatestTripEndDate = tripEnd;
            } else {
                earliestTripBeginDate = tripBegin.before(earliestTripBeginDate) ? tripBegin : earliestTripBeginDate;
                greatestTripEndDate = tripEnd.after(greatestTripEndDate) ? tripEnd : greatestTripEndDate;

            }
        }

        // TR with no TAs created from mainmenu
        if (documents.isEmpty() && ObjectUtils.isNotNull(travelDocument.getTripBegin())
                && ObjectUtils.isNotNull(travelDocument.getTripEnd())) {
            earliestTripBeginDate = getTripBeginDate(travelDocument.getTripBegin());
            greatestTripEndDate = getTripEndDate(travelDocument.getTripEnd());
        }

        List<TravelReimbursementDocument> matchDocs = (List<TravelReimbursementDocument>) travelDocumentDao
                .findMatchingTrips(temProfileId, earliestTripBeginDate, greatestTripEndDate);
        List<String> documentIds = new ArrayList<String>();
        for (TravelReimbursementDocument document : matchDocs) {
            if (!travelDocument.getDocumentNumber().equals(document.getDocumentNumber())) {
                documentIds.add(document.getDocumentNumber());
            }
        }
        return documentIds;
    }

    private Integer getDuplicateTripDateRangeDays() {
        String tripDateRangeDays = parameterService.getParameterValueAsString(TravelAuthorizationDocument.class,
                TemConstants.TravelParameters.DUPLICATE_TRIP_DATE_RANGE_DAYS);
        Integer days = null;
        if (!StringUtils.isNumeric(tripDateRangeDays)) {
            days = TemConstants.DEFAULT_DUPLICATE_TRIP_DATE_RANGE_DAYS;
        }

        days = Integer.parseInt(tripDateRangeDays);
        return days;

    }

    private Timestamp getTripBeginDate(Timestamp tripBeginDate) {
        Timestamp tripBegin = null;
        Integer days = getDuplicateTripDateRangeDays();
        try {
            tripBegin = dateTimeService.convertToSqlTimestamp(
                    dateTimeService.toDateString(DateUtils.addDays(tripBeginDate, (days * -1))));

        } catch (java.text.ParseException pe) {
            LOG.error("Exception while parsing trip begin date" + pe);
        }

        return tripBegin;

    }

    private Timestamp getTripEndDate(Timestamp tripEndDate) {
        Timestamp tripEnd = null;
        Integer days = getDuplicateTripDateRangeDays();
        try {
            tripEnd = dateTimeService
                    .convertToSqlTimestamp(dateTimeService.toDateString((DateUtils.addDays(tripEndDate, days))));

        } catch (java.text.ParseException pe) {
            LOG.error("Exception while parsing trip end date" + pe);
        }

        return tripEnd;

    }

    /**
     * Inner class to hold keys & messages for disabled properties
     */
    class DisabledPropertyMessage {
        private String key;
        private String message;

        DisabledPropertyMessage(String key, String message) {
            this.key = key;
            this.message = message;
        }

        void addToProperties(Map<String, String> messageMap) {
            messageMap.put(key, message);
        }
    }

    /**
     *
     * This method gets the current travel document by travel document identifier
     * @param travelDocumentIdentifier
     * @return
     */
    @Override
    public TravelDocument getParentTravelDocument(String travelDocumentIdentifier) {

        if (ObjectUtils.isNull(travelDocumentIdentifier) || StringUtils.equals(travelDocumentIdentifier, "")) {
            LOG.error("Received a null tripId/travelDocumentIdentifier; returning a null TravelDocument");
            return null;
        }

        try {
            TravelDocument travelDocument = findRootForTravelReimbursement(travelDocumentIdentifier);
            if (ObjectUtils.isNotNull(travelDocument)) {
                LOG.debug(
                        "Found " + travelDocument.getDocumentNumber() + " (" + travelDocument.getDocumentTypeName()
                                + ") for travelDocumentIdentifier: " + travelDocumentIdentifier);
                return travelDocument;
            }

        } catch (Exception exception) {
            LOG.error(
                    "Exception occurred attempting to retrieve an authorization or remibursement travel document for travelDocumentIdentifier: "
                            + travelDocumentIdentifier,
                    exception);
            return null;
        }

        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put(TemPropertyConstants.TRAVEL_DOCUMENT_IDENTIFIER, travelDocumentIdentifier);
        fieldValues.put(TemPropertyConstants.TRIP_PROGENITOR, Boolean.TRUE);

        Collection<TravelEntertainmentDocument> entDocuments = getBusinessObjectService()
                .findMatching(TravelEntertainmentDocument.class, fieldValues);
        if (entDocuments.iterator().hasNext()) {
            TravelDocument ent = entDocuments.iterator().next();
            LOG.debug("Found " + ent.getDocumentNumber() + " (" + ent.getDocumentTypeName()
                    + ") for travelDocumentIdentifier: " + travelDocumentIdentifier);
            return ent;
        }

        Collection<TravelRelocationDocument> reloDocuments = getBusinessObjectService()
                .findMatching(TravelRelocationDocument.class, fieldValues);
        if (reloDocuments.iterator().hasNext()) {
            TravelDocument relo = reloDocuments.iterator().next();
            LOG.info("Found " + relo.getDocumentNumber() + " (" + relo.getDocumentTypeName()
                    + ") for travelDocumentIdentifier: " + travelDocumentIdentifier);
            return relo;
        }

        LOG.error("Unable to find any travel document for given Trip Id: " + travelDocumentIdentifier);
        return null;
    }

    /**
     * Calculate the total of the source accounting lines on the document
     * @param travelDoc the travel document to calculate the source accounting line total for
     * @return the total of the source accounting lines
     */
    protected KualiDecimal getAccountingLineAmount(TravelDocument travelDoc) {
        KualiDecimal total = KualiDecimal.ZERO;
        if (travelDoc.getSourceAccountingLines() != null && !travelDoc.getSourceAccountingLines().isEmpty()) {
            for (TemSourceAccountingLine accountingLine : (List<TemSourceAccountingLine>) travelDoc
                    .getSourceAccountingLines()) {
                total = total.add(accountingLine.getAmount());
            }
        }
        return total;
    }

    /**
     *
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getTravelDocumentNumbersByTrip(java.lang.String)
     */
    @Override
    public Collection<String> getApprovedTravelDocumentNumbersByTrip(String travelDocumentIdentifier) {
        HashMap<String, String> documentNumbersToReturn = new HashMap<String, String>();

        List<TravelDocument> travelDocuments = new ArrayList<TravelDocument>();

        TravelDocument travelDocument = getParentTravelDocument(travelDocumentIdentifier);
        if (ObjectUtils.isNotNull(travelDocument)) {
            travelDocuments.add(travelDocument);
        }

        travelDocuments.addAll(
                getTravelDocumentDao().findDocuments(TravelReimbursementDocument.class, travelDocumentIdentifier));
        travelDocuments.addAll(
                getTravelDocumentDao().findDocuments(TravelEntertainmentDocument.class, travelDocumentIdentifier));
        travelDocuments.addAll(
                getTravelDocumentDao().findDocuments(TravelRelocationDocument.class, travelDocumentIdentifier));

        for (Iterator<TravelDocument> iter = travelDocuments.iterator(); iter.hasNext();) {
            TravelDocument document = iter.next();
            if (!documentNumbersToReturn.containsKey(document.getDocumentNumber())
                    && isDocumentStatusValidForReconcilingCharges(document)) {
                documentNumbersToReturn.put(document.getDocumentNumber(), document.getDocumentNumber());
            }
        }

        return documentNumbersToReturn.values();
    }

    @Override
    public boolean isDocumentStatusValidForReconcilingCharges(TravelDocument travelDocument) {

        String documentNumber = travelDocument.getDocumentNumber();

        if (isDocumentApprovedOrExtracted(documentNumber)) {
            return true;
        }

        if (travelDocument instanceof TravelAuthorizationDocument) {
            boolean vendorPaymentAllowedBeforeFinalAuthorization = getParameterService().getParameterValueAsBoolean(
                    TravelAuthorizationDocument.class,
                    TemConstants.TravelAuthorizationParameters.VENDOR_PAYMENT_ALLOWED_BEFORE_FINAL_APPROVAL_IND);

            if (vendorPaymentAllowedBeforeFinalAuthorization) {
                return isDocumentInitiatedOrEnroute(documentNumber);
            }
        }

        if (travelDocument instanceof TravelReimbursementDocument) {
            boolean vendorPaymentAllowedBeforeFinalReimbursement = getParameterService().getParameterValueAsBoolean(
                    TravelReimbursementDocument.class,
                    TemConstants.TravelAuthorizationParameters.VENDOR_PAYMENT_ALLOWED_BEFORE_FINAL_APPROVAL_IND);

            if (vendorPaymentAllowedBeforeFinalReimbursement) {
                return isDocumentInitiatedOrEnroute(documentNumber);
            }
        }

        return false;
    }

    /**
     *
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#restorePerDiemProperty(org.kuali.kfs.module.tem.document.TravelDocument, java.lang.String)
     */
    @Override
    public void restorePerDiemProperty(TravelDocument document, String property) {
        try {
            final String[] perDiemPropertyParts = splitPerDiemProperty(property);
            PerDiemExpense perDiemExpense = (PerDiemExpense) ObjectUtils.getPropertyValue(document,
                    perDiemPropertyParts[0]);
            final String mealName = perDiemPropertyParts[1];
            final boolean mealProperty = isMealProperty(mealName);
            final String mealSuffix = (mealProperty) ? "Value" : "";
            final String mealValueName = mealName + mealSuffix;

            KualiDecimal currentMealValue = (KualiDecimal) ObjectUtils.getPropertyValue(perDiemExpense,
                    mealValueName);
            if (currentMealValue != null && currentMealValue.equals(KualiDecimal.ZERO)) {
                final PerDiem perDiem = getPerDiemService().getPerDiem(perDiemExpense.getPrimaryDestinationId(),
                        perDiemExpense.getMileageDate(), document.getEffectiveDateForPerDiem(perDiemExpense));
                final KualiDecimal mealAmount = (KualiDecimal) ObjectUtils.getPropertyValue(perDiem, mealName);
                final boolean prorated = mealProperty
                        && !KfsDateUtils.isSameDay(document.getTripBegin(), document.getTripEnd())
                        && (KfsDateUtils.isSameDay(perDiemExpense.getMileageDate(), document.getTripBegin())
                                || KfsDateUtils.isSameDay(perDiemExpense.getMileageDate(), document.getTripEnd()));
                if (prorated && !ObjectUtils.isNull(document.getTripType())) {
                    perDiemExpense.setProrated(true);
                    final String perDiemCalcMethod = document.getTripType().getPerDiemCalcMethod();
                    final Integer perDiemPercent = calculateProratePercentage(perDiemExpense, perDiemCalcMethod,
                            document.getTripEnd());
                    final KualiDecimal proratedAmount = PerDiemExpense
                            .calculateMealsAndIncidentalsProrated(mealAmount, perDiemPercent);
                    ObjectUtils.setObjectProperty(perDiemExpense, mealValueName, proratedAmount);
                } else {
                    ObjectUtils.setObjectProperty(perDiemExpense, mealValueName, mealAmount);
                }
                if (mealProperty) {
                    ObjectUtils.setObjectProperty(perDiemExpense, mealName, Boolean.TRUE);
                }
            }
        } catch (FormatException fe) {
            throw new RuntimeException("Could not set meal value on per diem expense", fe);
        } catch (IllegalAccessException iae) {
            throw new RuntimeException("Could not set meal value on per diem expense", iae);
        } catch (InvocationTargetException ite) {
            throw new RuntimeException("Could not set meal value on per diem expense", ite);
        } catch (NoSuchMethodException nsme) {
            throw new RuntimeException("Could not set meal value on per diem expense", nsme);
        }
    }

    /**
     * Determines if the given property name represents a meal on a PerDiemExpense (ie, a property with a boolean property and a "Value" property)
     * @param property the property to check
     * @return true if the property represents a field with an extra "Value" field, false otherwise
     */
    protected boolean isMealProperty(String property) {
        return StringUtils.equals(property, TemPropertyConstants.BREAKFAST)
                || StringUtils.equals(property, TemPropertyConstants.LUNCH)
                || StringUtils.equals(property, TemPropertyConstants.DINNER)
                || StringUtils.equals(property, TemPropertyConstants.INCIDENTALS);
    }

    /**
     * Splits a property into the per diem part and the property of the per diem expense we should update
     * @param property the property to split
     * @return an Array where the first element is the property path to a per diem expense and the second is the property path to a meal value on that per diem
     */
    protected String[] splitPerDiemProperty(String property) {
        final String deDocumentedProperty = property.replace(KFSPropertyConstants.DOCUMENT + ".",
                KFSConstants.EMPTY_STRING);
        final int lastDivider = deDocumentedProperty.lastIndexOf('.');
        final String perDiemPart = deDocumentedProperty.substring(0, lastDivider);
        final String mealPart = deDocumentedProperty.substring(lastDivider + 1);
        return new String[] { perDiemPart, mealPart };
    }

    /**
     * Looks up the document with the progenitor document for the trip
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getRootTravelDocumentWithoutWorkflowDocument(java.lang.String)
     */
    @Override
    public TravelDocument getRootTravelDocumentWithoutWorkflowDocument(String travelDocumentIdentifier) {
        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put(TemPropertyConstants.TRAVEL_DOCUMENT_IDENTIFIER, travelDocumentIdentifier);
        fieldValues.put(TemPropertyConstants.TRIP_PROGENITOR, new Boolean(true));
        for (String documentType : getTravelDocumentTypesToCheck()) {
            final Class<? extends TravelDocument> docClazz = getTravelDocumentForType(documentType);
            Collection<TravelDocument> matchingDocs = (Collection<TravelDocument>) getBusinessObjectService()
                    .findMatching(docClazz, fieldValues);
            if (matchingDocs != null && !matchingDocs.isEmpty()) {
                List<TravelDocument> foundDocs = new ArrayList<TravelDocument>();
                foundDocs.addAll(matchingDocs);
                return foundDocs.get(0);
            }
        }
        return null;
    }

    /**
     * HEY EVERYONE! BIG CUSTOMIZATION OPPORTUNITY!
     * This method returns an ordered list of where to look for progenitor documents.  The order is based on my total guess of which
     * document type is most likely to be the progenitor, so it's TA, ENT, RELO, TR.  But, if you don't use TA's, then obviously TR's should
     * be first.  Anyhow, please feel free to rearrange this list as seems most helpful to you
     * @return a List of the document types to look for root documents in - in which order
     */
    protected List<String> getTravelDocumentTypesToCheck() {
        List<String> documentTypes = new ArrayList<String>();
        documentTypes.add(TemConstants.TravelDocTypes.TRAVEL_AUTHORIZATION_DOCUMENT);
        documentTypes.add(TemConstants.TravelDocTypes.TRAVEL_ENTERTAINMENT_DOCUMENT);
        documentTypes.add(TemConstants.TravelDocTypes.TRAVEL_RELOCATION_DOCUMENT);
        documentTypes.add(TemConstants.TravelDocTypes.TRAVEL_REIMBURSEMENT_DOCUMENT);
        return documentTypes;
    }

    /**
     * Looks up the class associated with the given document type to check
     * @param documentType the document type name to find a class for
     * @return the class of that document type
     */
    protected Class<? extends TravelDocument> getTravelDocumentForType(String documentType) {
        return (Class<TravelDocument>) getDataDictionaryService().getDocumentClassByTypeName(documentType);
    }

    /**
     * This smooshes the accounting lines which will do advance clearing.  Here, since we're replacing the object code, we'll smooth together all accounting lines
     * which have the same chart - account - sub-acount.
     * @param originalAccountingLines the List of accounting lines to smoosh
     * @return the smooshed accounting lines
     */
    @Override
    public List<TemSourceAccountingLine> smooshAccountingLinesToSubAccount(
            List<TemSourceAccountingLine> originalAccountingLines) {
        final Map<SmooshLineKey, KualiDecimal> smooshLines = smooshLinesToMap(originalAccountingLines);
        final List<TemSourceAccountingLine> unsmooshedLines = raiseMapToLines(smooshLines);
        return unsmooshedLines;
    }

    /**
     * Smooshes the lines into a Map
     * @param accountingLines the accounting lines to smoosh
     * @return the Map of smooshed lines
     */
    protected Map<SmooshLineKey, KualiDecimal> smooshLinesToMap(List<TemSourceAccountingLine> accountingLines) {
        Map<SmooshLineKey, KualiDecimal> smooshLines = new HashMap<SmooshLineKey, KualiDecimal>();
        for (TemSourceAccountingLine line : accountingLines) {
            final SmooshLineKey key = new SmooshLineKey(line);
            if (smooshLines.containsKey(key)) {
                KualiDecimal currAmount = smooshLines.get(key);
                KualiDecimal newAmount = currAmount.add(line.getAmount());
                smooshLines.put(key, newAmount);
            } else {
                smooshLines.put(key, line.getAmount());
            }
        }
        return smooshLines;
    }

    /**
     * According to thesaurus.com, "raise" is the antonym of "smoosh".  So this method takes our smooshed line information and turns them back into things which sort of resemble accounting lines
     * @param smooshLineMap the Map to turn back into accounting lines
     * @return the un-smooshed accounting lines.  Yeah, I like that verb better too
     */
    protected List<TemSourceAccountingLine> raiseMapToLines(Map<SmooshLineKey, KualiDecimal> smooshLineMap) {
        List<TemSourceAccountingLine> raisedLines = new ArrayList<TemSourceAccountingLine>();
        for (SmooshLineKey key : smooshLineMap.keySet()) {
            final TemSourceAccountingLine line = convertKeyAndAmountToLine(key, smooshLineMap.get(key));
            raisedLines.add(line);
        }
        return raisedLines;
    }

    /**
     * Converts a SmooshLineKey and an amount into a real - though somewhat less informative - accounting line
     * @param key the key
     * @param amount the amount
     * @return the reconstituted accounting line.  I like that verb too.
     */
    protected TemSourceAccountingLine convertKeyAndAmountToLine(SmooshLineKey key, KualiDecimal amount) {
        TemSourceAccountingLine line = new TemSourceAccountingLine();
        line.setChartOfAccountsCode(key.getChartOfAccountsCode());
        line.setAccountNumber(key.getAccountNumber());
        line.setSubAccountNumber(key.getSubAccountNumber());
        line.setAmount(amount);
        return line;
    }

    /**
     * Hash key of lines we want to smoosh
     */
    protected class SmooshLineKey {
        protected String chartOfAccountsCode;
        protected String accountNumber;
        protected String subAccountNumber;

        public SmooshLineKey(TemSourceAccountingLine accountingLine) {
            this.chartOfAccountsCode = accountingLine.getChartOfAccountsCode();
            this.accountNumber = accountingLine.getAccountNumber();
            this.subAccountNumber = accountingLine.getSubAccountNumber();
        }

        public String getChartOfAccountsCode() {
            return chartOfAccountsCode;
        }

        public String getAccountNumber() {
            return accountNumber;
        }

        public String getSubAccountNumber() {
            return subAccountNumber;
        }

        @Override
        public int hashCode() {
            HashCodeBuilder hcb = new HashCodeBuilder();
            hcb.append(getChartOfAccountsCode());
            hcb.append(getAccountNumber());
            hcb.append(getSubAccountNumber());
            return hcb.toHashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof SmooshLineKey) || obj == null) {
                return false;
            }
            final SmooshLineKey golyadkin = (SmooshLineKey) obj;
            EqualsBuilder eb = new EqualsBuilder();
            eb.append(getChartOfAccountsCode(), golyadkin.getChartOfAccountsCode());
            eb.append(getAccountNumber(), golyadkin.getAccountNumber());
            eb.append(getSubAccountNumber(), golyadkin.getSubAccountNumber());
            return eb.isEquals();
        }
    }

    /**
     * Parses the value of url.document.travelRelocation.agencySites and turns those into links
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getAgencyLinks(org.kuali.kfs.module.tem.document.TravelDocument)
     */
    @Override
    public List<LinkField> getAgencyLinks(TravelDocument travelDocument) {
        List<LinkField> agencyLinks = new ArrayList<LinkField>();
        if (getConfigurationService().getPropertyValueAsBoolean(TemKeyConstants.ENABLE_AGENCY_SITES_URL)) {
            final String agencySitesURL = getConfigurationService()
                    .getPropertyValueAsString(TemKeyConstants.AGENCY_SITES_URL);
            final String target = "_blank";
            if (!StringUtils.isEmpty(agencySitesURL)) {
                String[] sites = agencySitesURL.split(";");
                for (String site : sites) {
                    String[] siteInfo = site.split("=");
                    String url = customizeAgencyLink(travelDocument, siteInfo[0], siteInfo[1]);
                    final String prefixedUrl = prefixUrl(url);
                    LinkField link = new LinkField();
                    link.setHrefText(prefixedUrl);
                    link.setTarget(target);
                    link.setLinkLabel(siteInfo[0]);
                    agencyLinks.add(link);
                }
            }
        }
        return agencyLinks;
    }

    /**
     * In the default version, checks if the "config.document.travelRelocation.agencySites.include.tripId" property is true and if it is, just dumbly
     * appends the tripId= doc's trip id to the link.  Really, out of the box, this isn't all that smart. Will mask the value if the parameter says to.
     * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#customizeAgencyLink(org.kuali.kfs.module.tem.document.TravelDocument, java.lang.String, java.lang.String)
     */
    @Override
    public String customizeAgencyLink(TravelDocument travelDocument, String agencyName, String link) {
        final boolean passTrip = getConfigurationService()
                .getPropertyValueAsBoolean(TemKeyConstants.PASS_TRIP_ID_TO_AGENCY_SITES);
        if (!passTrip || StringUtils.isBlank(travelDocument.getTravelDocumentIdentifier())) {
            return link; // nothing to add
        }

        if (travelDocument instanceof TravelAuthorizationDocument) {
            final boolean vendorPaymentAllowedBeforeFinal = getParameterService().getParameterValueAsBoolean(
                    TravelAuthorizationDocument.class,
                    TemConstants.TravelAuthorizationParameters.VENDOR_PAYMENT_ALLOWED_BEFORE_FINAL_APPROVAL_IND);
            if (!vendorPaymentAllowedBeforeFinal) {
                return link;
            }
        }
        final String linkWithTripId = link + "?tripId=" + travelDocument.getTravelDocumentIdentifier();
        return linkWithTripId;
    }

    /**
     * Makes sure that url starts with https
     * @param url the url to prefix as needed
     * @return the url prefixed by protocol
     */
    protected String prefixUrl(String url) {
        String prefixedUrl = url;
        if (!prefixedUrl.startsWith("http")) {
            prefixedUrl = "https://" + prefixedUrl;
        }
        return prefixedUrl;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelArrangerDocumentService#isInitiatorTraveler(TravelDocument)
     */
    @Override
    public boolean isInitiatorTraveler(TravelDocument travelDoc) {
        String initiatorId = travelDoc.getDocumentHeader().getWorkflowDocument().getInitiatorPrincipalId();
        String travelerId = travelDoc.getTraveler().getPrincipalId();
        boolean is = travelerId != null && initiatorId.equals(travelerId);
        return is;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelArrangerDocumentService#requiresTravelerApproval(TravelAuthorizationDocument)
     */
    @Override
    public boolean requiresTravelerApproval(TravelAuthorizationDocument taDoc) {
        // If there's travel advances, route to traveler if necessary
        boolean require = taDoc.requiresTravelAdvanceReviewRouting();
        require &= !taDoc.getTravelAdvance().getTravelAdvancePolicy();

        return require;
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.TravelArrangerDocumentService#requiresTravelerApproval(TEMReimbursementDocument)
     */
    @Override
    public boolean requiresTravelerApproval(TEMReimbursementDocument trDoc) {
        String travelerTypeCode = trDoc.getTraveler().getTravelerTypeCode();
        if (parameterService.getParameterValuesAsString(TemParameterConstants.TEM_DOCUMENT.class,
                TravelParameters.NON_EMPLOYEE_TRAVELER_TYPE_CODES).contains(travelerTypeCode)) {
            return false;
        }

        // no need to route back to traveler if s/he is the initiator
        return !isInitiatorTraveler(trDoc);
    }

    /**
     * @return the system-ste implementation of the AccountsReceivableModuleService
     */
    public AccountsReceivableModuleService getAccountsReceivableModuleService() {
        if (accountsReceivableModuleService == null) {
            accountsReceivableModuleService = SpringContext.getBean(AccountsReceivableModuleService.class);
        }
        return accountsReceivableModuleService;
    }

    public TravelAuthorizationService getTravelAuthorizationService() {
        return travelAuthorizationService;
    }

    public void setTravelAuthorizationService(TravelAuthorizationService travelAuthorizationService) {
        this.travelAuthorizationService = travelAuthorizationService;
    }

    public PerDiemService getPerDiemService() {
        return perDiemService;
    }

    public void setPerDiemService(PerDiemService perDiemService) {
        this.perDiemService = perDiemService;
    }

    public List<String> getGroupTravelerColumns() {
        return groupTravelerColumns;
    }

    public void setGroupTravelerColumns(List<String> groupTravelerColumns) {
        this.groupTravelerColumns = groupTravelerColumns;
    }

    public TravelExpenseService getTravelExpenseService() {
        return travelExpenseService;
    }

    public void setTravelExpenseService(TravelExpenseService travelExpenseService) {
        this.travelExpenseService = travelExpenseService;
    }

    public NoteService getNoteService() {
        return noteService;
    }

    public void setNoteService(NoteService noteService) {
        this.noteService = noteService;
    }

    public TravelService getTravelService() {
        return travelService;
    }

    public void setTravelService(TravelService travelService) {
        this.travelService = travelService;
    }

    public MileageRateService getMileageRateService() {
        return mileageRateService;
    }

    public void setMileageRateService(MileageRateService mileageRateService) {
        this.mileageRateService = mileageRateService;
    }

    public void setDocumentService(DocumentService documentService) {
        this.documentService = documentService;
    }

    protected DocumentService getDocumentService() {
        return documentService;
    }

    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
        this.dataDictionaryService = dataDictionaryService;
    }

    protected DataDictionaryService getDataDictionaryService() {
        return dataDictionaryService;
    }

    public void setDateTimeService(final DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    protected DateTimeService getDateTimeService() {
        return dateTimeService;
    }

    public void setTravelDocumentDao(final TravelDocumentDao travelDocumentDao) {
        this.travelDocumentDao = travelDocumentDao;
    }

    protected TravelDocumentDao getTravelDocumentDao() {
        return travelDocumentDao;
    }

    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    protected BusinessObjectService getBusinessObjectService() {
        return businessObjectService;
    }

    public ParameterService getParameterService() {
        return parameterService;
    }

    public void setParameterService(ParameterService parameterService) {
        this.parameterService = parameterService;
    }

    public AccountingDocumentRelationshipService getAccountingDocumentRelationshipService() {
        return accountingDocumentRelationshipService;
    }

    public void setAccountingDocumentRelationshipService(
            AccountingDocumentRelationshipService accountingDocumentRelationshipService) {
        this.accountingDocumentRelationshipService = accountingDocumentRelationshipService;
    }

    public TemRoleService getTemRoleService() {
        return temRoleService;
    }

    public void setTemRoleService(TemRoleService temRoleService) {
        this.temRoleService = temRoleService;
    }

    protected ConfigurationService getConfigurationService() {
        return configurationService;
    }

    public void setConfigurationService(ConfigurationService configurationService) {
        this.configurationService = configurationService;
    }

    public StateService getStateService() {
        return stateService;
    }

    public void setStateService(StateService stateService) {
        this.stateService = stateService;
    }

    /**
     * Gets the universityDateService attribute.
     * @return Returns the universityDateService.
     */
    public UniversityDateService getUniversityDateService() {
        return universityDateService;
    }

    /**
     * Sets the universityDateService attribute value.
     * @param universityDateService The universityDateService to set.
     */
    public void setUniversityDateService(UniversityDateService universityDateService) {
        this.universityDateService = universityDateService;
    }

    public List<String> getDefaultAcceptableFileExtensions() {
        return defaultAcceptableFileExtensions;
    }

    public void setDefaultAcceptableFileExtensions(final List<String> defaultAcceptableFileExtensions) {
        this.defaultAcceptableFileExtensions = defaultAcceptableFileExtensions;
    }

    public void setCsvRecordFactory(final CsvRecordFactory<GroupTravelerCsvRecord> recordFactory) {
        this.csvRecordFactory = recordFactory;
    }

    public CsvRecordFactory<GroupTravelerCsvRecord> getCsvRecordFactory() {
        return this.csvRecordFactory;
    }
}