/* * 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; } }