View Javadoc
1   /*
2    * The Kuali Financial System, a comprehensive financial management system for higher education.
3    * 
4    * Copyright 2005-2014 The Kuali Foundation
5    * 
6    * This program is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU Affero General Public License as
8    * published by the Free Software Foundation, either version 3 of the
9    * License, or (at your option) any later version.
10   * 
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU Affero General Public License for more details.
15   * 
16   * You should have received a copy of the GNU Affero General Public License
17   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18   */
19  package org.kuali.kfs.module.tem.document.service.impl;
20  
21  import static org.kuali.kfs.module.tem.TemKeyConstants.ERROR_UPLOADPARSER_INVALID_NUMERIC_VALUE;
22  import static org.kuali.kfs.module.tem.TemKeyConstants.ERROR_UPLOADPARSER_LINE;
23  import static org.kuali.kfs.module.tem.TemKeyConstants.ERROR_UPLOADPARSER_PROPERTY;
24  import static org.kuali.kfs.module.tem.TemKeyConstants.ERROR_UPLOADPARSER_WRONG_PROPERTY_NUMBER;
25  import static org.kuali.kfs.module.tem.TemKeyConstants.MESSAGE_TR_LODGING_ALREADY_CLAIMED;
26  import static org.kuali.kfs.module.tem.TemKeyConstants.MESSAGE_TR_MEAL_ALREADY_CLAIMED;
27  import static org.kuali.kfs.module.tem.TemKeyConstants.MESSAGE_UPLOADPARSER_EXCEEDED_MAX_LENGTH;
28  import static org.kuali.kfs.module.tem.TemKeyConstants.MESSAGE_UPLOADPARSER_INVALID_VALUE;
29  import static org.kuali.kfs.module.tem.TemPropertyConstants.PER_DIEM_EXPENSE_DISABLED;
30  
31  import java.io.BufferedReader;
32  import java.io.IOException;
33  import java.io.StringReader;
34  import java.lang.reflect.InvocationTargetException;
35  import java.math.BigDecimal;
36  import java.sql.Date;
37  import java.sql.Timestamp;
38  import java.text.MessageFormat;
39  import java.text.SimpleDateFormat;
40  import java.util.ArrayList;
41  import java.util.Arrays;
42  import java.util.Calendar;
43  import java.util.Collection;
44  import java.util.Collections;
45  import java.util.Comparator;
46  import java.util.GregorianCalendar;
47  import java.util.HashMap;
48  import java.util.HashSet;
49  import java.util.Iterator;
50  import java.util.LinkedList;
51  import java.util.List;
52  import java.util.Map;
53  import java.util.Set;
54  import java.util.TreeSet;
55  
56  import org.apache.commons.lang.StringUtils;
57  import org.apache.commons.lang.builder.EqualsBuilder;
58  import org.apache.commons.lang.builder.HashCodeBuilder;
59  import org.apache.commons.lang.time.DateUtils;
60  import org.apache.log4j.Logger;
61  import org.kuali.kfs.integration.ar.AccountsReceivableCustomerInvoice;
62  import org.kuali.kfs.integration.ar.AccountsReceivableModuleService;
63  import org.kuali.kfs.integration.ar.AccountsReceivableOrganizationOptions;
64  import org.kuali.kfs.module.tem.TemConstants;
65  import org.kuali.kfs.module.tem.TemConstants.TravelAuthorizationParameters;
66  import org.kuali.kfs.module.tem.TemConstants.TravelAuthorizationStatusCodeKeys;
67  import org.kuali.kfs.module.tem.TemConstants.TravelDocTypes;
68  import org.kuali.kfs.module.tem.TemConstants.TravelParameters;
69  import org.kuali.kfs.module.tem.TemKeyConstants;
70  import org.kuali.kfs.module.tem.TemParameterConstants;
71  import org.kuali.kfs.module.tem.TemPropertyConstants;
72  import org.kuali.kfs.module.tem.TemWorkflowConstants;
73  import org.kuali.kfs.module.tem.businessobject.ActualExpense;
74  import org.kuali.kfs.module.tem.businessobject.ExpenseType;
75  import org.kuali.kfs.module.tem.businessobject.ExpenseTypeAware;
76  import org.kuali.kfs.module.tem.businessobject.GroupTraveler;
77  import org.kuali.kfs.module.tem.businessobject.GroupTravelerCsvRecord;
78  import org.kuali.kfs.module.tem.businessobject.HistoricalTravelExpense;
79  import org.kuali.kfs.module.tem.businessobject.ImportedExpense;
80  import org.kuali.kfs.module.tem.businessobject.MileageRate;
81  import org.kuali.kfs.module.tem.businessobject.PerDiem;
82  import org.kuali.kfs.module.tem.businessobject.PerDiemExpense;
83  import org.kuali.kfs.module.tem.businessobject.PrimaryDestination;
84  import org.kuali.kfs.module.tem.businessobject.SpecialCircumstances;
85  import org.kuali.kfs.module.tem.businessobject.SpecialCircumstancesQuestion;
86  import org.kuali.kfs.module.tem.businessobject.TemExpense;
87  import org.kuali.kfs.module.tem.businessobject.TemRegion;
88  import org.kuali.kfs.module.tem.businessobject.TemSourceAccountingLine;
89  import org.kuali.kfs.module.tem.businessobject.TransportationModeDetail;
90  import org.kuali.kfs.module.tem.businessobject.TravelAdvance;
91  import org.kuali.kfs.module.tem.businessobject.TripType;
92  import org.kuali.kfs.module.tem.dataaccess.TravelDocumentDao;
93  import org.kuali.kfs.module.tem.document.TEMReimbursementDocument;
94  import org.kuali.kfs.module.tem.document.TravelAuthorizationDocument;
95  import org.kuali.kfs.module.tem.document.TravelDocument;
96  import org.kuali.kfs.module.tem.document.TravelEntertainmentDocument;
97  import org.kuali.kfs.module.tem.document.TravelReimbursementDocument;
98  import org.kuali.kfs.module.tem.document.TravelRelocationDocument;
99  import org.kuali.kfs.module.tem.document.service.AccountingDocumentRelationshipService;
100 import org.kuali.kfs.module.tem.document.service.MileageRateService;
101 import org.kuali.kfs.module.tem.document.service.TravelAuthorizationService;
102 import org.kuali.kfs.module.tem.document.service.TravelDocumentService;
103 import org.kuali.kfs.module.tem.document.web.struts.TravelFormBase;
104 import org.kuali.kfs.module.tem.exception.UploadParserException;
105 import org.kuali.kfs.module.tem.service.CsvRecordFactory;
106 import org.kuali.kfs.module.tem.service.PerDiemService;
107 import org.kuali.kfs.module.tem.service.TemRoleService;
108 import org.kuali.kfs.module.tem.service.TravelExpenseService;
109 import org.kuali.kfs.module.tem.service.TravelService;
110 import org.kuali.kfs.module.tem.util.ExpenseUtils;
111 import org.kuali.kfs.sys.KFSConstants;
112 import org.kuali.kfs.sys.KFSKeyConstants;
113 import org.kuali.kfs.sys.KFSPropertyConstants;
114 import org.kuali.kfs.sys.businessobject.AccountingLine;
115 import org.kuali.kfs.sys.businessobject.FinancialSystemDocumentHeader;
116 import org.kuali.kfs.sys.businessobject.PaymentDocumentationLocation;
117 import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
118 import org.kuali.kfs.sys.context.SpringContext;
119 import org.kuali.kfs.sys.exception.ParseException;
120 import org.kuali.kfs.sys.service.UniversityDateService;
121 import org.kuali.kfs.sys.util.KfsDateUtils;
122 import org.kuali.rice.core.api.config.property.ConfigurationService;
123 import org.kuali.rice.core.api.datetime.DateTimeService;
124 import org.kuali.rice.core.api.util.ConcreteKeyValue;
125 import org.kuali.rice.core.api.util.KeyValue;
126 import org.kuali.rice.core.api.util.type.KualiDecimal;
127 import org.kuali.rice.core.web.format.FormatException;
128 import org.kuali.rice.coreservice.framework.parameter.ParameterService;
129 import org.kuali.rice.kew.api.KewApiConstants;
130 import org.kuali.rice.kew.api.KewApiServiceLocator;
131 import org.kuali.rice.kew.api.WorkflowDocument;
132 import org.kuali.rice.kew.api.action.ActionRequestType;
133 import org.kuali.rice.kew.api.document.attribute.DocumentAttributeIndexingQueue;
134 import org.kuali.rice.kew.api.exception.WorkflowException;
135 import org.kuali.rice.kim.api.identity.Person;
136 import org.kuali.rice.kim.api.identity.PersonService;
137 import org.kuali.rice.kim.api.identity.principal.Principal;
138 import org.kuali.rice.kim.api.services.KimApiServiceLocator;
139 import org.kuali.rice.kns.document.authorization.DocumentAuthorizer;
140 import org.kuali.rice.kns.service.DocumentHelperService;
141 import org.kuali.rice.kns.util.KNSGlobalVariables;
142 import org.kuali.rice.krad.bo.AdHocRoutePerson;
143 import org.kuali.rice.krad.bo.Note;
144 import org.kuali.rice.krad.bo.PersistableBusinessObject;
145 import org.kuali.rice.krad.document.Document;
146 import org.kuali.rice.krad.exception.InfrastructureException;
147 import org.kuali.rice.krad.service.BusinessObjectService;
148 import org.kuali.rice.krad.service.DataDictionaryService;
149 import org.kuali.rice.krad.service.DocumentService;
150 import org.kuali.rice.krad.service.NoteService;
151 import org.kuali.rice.krad.service.SequenceAccessorService;
152 import org.kuali.rice.krad.uif.field.LinkField;
153 import org.kuali.rice.krad.util.GlobalVariables;
154 import org.kuali.rice.krad.util.KRADPropertyConstants;
155 import org.kuali.rice.krad.util.ObjectUtils;
156 import org.kuali.rice.location.api.state.State;
157 import org.kuali.rice.location.api.state.StateService;
158 import org.springframework.beans.BeanUtils;
159 import org.springframework.transaction.annotation.Transactional;
160 
161 import au.com.bytecode.opencsv.CSVReader;
162 
163 
164 /**
165  * Travel Service Implementation
166  */
167 @Transactional
168 public class TravelDocumentServiceImpl implements TravelDocumentService {
169 
170     protected static Logger LOG = Logger.getLogger(TravelDocumentServiceImpl.class);
171 
172     protected DataDictionaryService dataDictionaryService;
173     protected DocumentService documentService;
174     protected BusinessObjectService businessObjectService;
175     protected TravelDocumentDao travelDocumentDao;
176     protected TravelAuthorizationService travelAuthorizationService;
177     protected DateTimeService dateTimeService;
178     protected ParameterService parameterService;
179     protected AccountingDocumentRelationshipService accountingDocumentRelationshipService;
180     protected TemRoleService temRoleService;
181     protected StateService stateService;
182     protected ConfigurationService configurationService;
183     protected UniversityDateService universityDateService;
184     protected List<String> defaultAcceptableFileExtensions;
185     protected CsvRecordFactory<GroupTravelerCsvRecord> csvRecordFactory;
186     protected List<String> groupTravelerColumns;
187     protected volatile AccountsReceivableModuleService accountsReceivableModuleService;
188     protected PerDiemService perDiemService;
189     protected TravelExpenseService travelExpenseService;
190     protected NoteService noteService;
191     protected TravelService travelService;
192     protected MileageRateService mileageRateService;
193 
194 
195     /**
196      * Creates and populates an individual per diem item.
197      *
198      * @param perDiemId is the id for the referenced {@link PerDiem} object that gets attached
199      * @return date of the item
200      */
201     protected PerDiemExpense createPerDiemItem(final TravelDocument document, final PerDiem newPerDiem, final Timestamp ts, final boolean prorated, String mileageRateExpenseTypeCode) {
202         final PerDiemExpense expense = newPerDiemExpense();
203         expense.setPrimaryDestinationId(newPerDiem.getPrimaryDestinationId());
204         expense.setProrated(prorated);
205         expense.setMileageDate(ts);
206 
207         expense.setPrimaryDestination(newPerDiem.getPrimaryDestination().getPrimaryDestinationName());
208         expense.setCountryState(newPerDiem.getPrimaryDestination().getRegion().getRegionName());
209         expense.setCounty(newPerDiem.getPrimaryDestination().getCounty());
210 
211         setPerDiemMealsAndIncidentals(expense, newPerDiem, document.getTripType(), document.getTripEnd(), expense.isProrated());
212         final KualiDecimal lodgingAmount = getPerDiemService().isPerDiemHandlingLodging() && !KfsDateUtils.isSameDay(document.getTripEnd(), ts) ? newPerDiem.getLodging() : KualiDecimal.ZERO;
213         expense.setLodging(lodgingAmount);
214         expense.setMileageRateExpenseTypeCode(mileageRateExpenseTypeCode);
215         return expense;
216     }
217 
218     /**
219      * returns a new instance of a PerDiemExpense turned into a service call so that we can provide our own instance during testing
220      */
221     protected PerDiemExpense newPerDiemExpense() {
222         return new PerDiemExpense();
223     }
224 
225     /**
226      * Sets the meal and incidental amounts on the given per diem expense
227      * @param expense the expense to set amounts on
228      * @param perDiem the per diem record amounts are based off of
229      * @param tripType the trip type being taken
230      * @param tripEnd the end time of the trip
231      * @param shouldProrate whether this expense should be prorated
232      */
233     @Override
234     public void setPerDiemMealsAndIncidentals(PerDiemExpense expense, PerDiem perDiem, TripType tripType, Timestamp tripEnd, boolean shouldProrate) {
235         String perDiemCalcMethod = null;
236         if (!ObjectUtils.isNull(tripType)) {
237             perDiemCalcMethod = tripType.getPerDiemCalcMethod();
238         }
239         //default first to per diem's values
240         expense.setBreakfastValue(perDiem.getBreakfast());
241         expense.setLunchValue(perDiem.getLunch());
242         expense.setDinnerValue(perDiem.getDinner());
243         expense.setIncidentalsValue(perDiem.getIncidentals());
244         // if prorated, recalculate the values
245         if(shouldProrate){
246             Integer perDiemPercent = calculateProratePercentage(expense, perDiemCalcMethod, tripEnd);
247             expense.setDinnerValue(PerDiemExpense.calculateMealsAndIncidentalsProrated(expense.getDinnerValue(), perDiemPercent));
248             expense.setLunchValue(PerDiemExpense.calculateMealsAndIncidentalsProrated(expense.getLunchValue(), perDiemPercent));
249             expense.setBreakfastValue(PerDiemExpense.calculateMealsAndIncidentalsProrated(expense.getBreakfastValue(), perDiemPercent));
250             expense.setIncidentalsValue(PerDiemExpense.calculateMealsAndIncidentalsProrated(expense.getIncidentalsValue(), perDiemPercent));
251 
252             correctProratedPerDiemExpense(expense, perDiemPercent, perDiem);
253         }
254     }
255 
256     /**
257      * 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.
258      * Extra change will be taken from breakfast.
259      * @param expense the expense to correct
260      * @param perDiemPercent the percentage of the proration for this per diem
261      * @param perDiem the per diem record to work against
262      */
263     protected void correctProratedPerDiemExpense(PerDiemExpense expense, Integer perDiemPercent, PerDiem perDiem) {
264         final KualiDecimal mealAndIncidentalLimit = PerDiemExpense.calculateMealsAndIncidentalsProrated(perDiem.getMealsAndIncidentals(), perDiemPercent);
265         if (expense.getMealsAndIncidentals().isGreaterThan(mealAndIncidentalLimit)) {
266             // take the difference from breakfast
267             final KualiDecimal delta = expense.getMealsAndIncidentals().subtract(mealAndIncidentalLimit);
268             expense.setBreakfastValue(expense.getBreakfastValue().subtract(delta));
269         }
270     }
271 
272     /**
273      * Creates a date range for iterating over
274      *
275      * @param start of the date range
276      * @param end of the date range
277      * @return Collection for iterating
278      */
279     protected Collection<Timestamp> dateRange(final Timestamp start, final Timestamp end) {
280         final Collection<Timestamp> retval = new ArrayList<Timestamp>();
281 
282         if (start != null && end != null) {
283             final Calendar cal = getDateTimeService().getCurrentCalendar();
284             cal.setTime(start);
285 
286             for (; !cal.getTime().after(end) || KfsDateUtils.isSameDay(cal.getTime(), end); cal.add(Calendar.DATE, 1)) {
287                 if (KfsDateUtils.isSameDay(cal.getTime(), end)) {
288                     retval.add(new Timestamp(end.getTime()));
289                 }
290                 else {
291                     retval.add(new Timestamp(cal.getTime().getTime()));
292                 }
293             }
294         }
295 
296         return retval;
297     }
298 
299     /**
300      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#updatePerDiemItemsFor(String, List, Integer, Timestamp, Timestamp)
301      */
302     @SuppressWarnings("null")
303     @Override
304     public void updatePerDiemItemsFor(final TravelDocument document, final List<PerDiemExpense> perDiemExpenseList, final Integer perDiemId, final Timestamp start, final Timestamp end) {
305         final String mileageRateExpenseTypeCode = getParameterService().getParameterValueAsString(TemParameterConstants.TEM_DOCUMENT.class, TemConstants.TravelParameters.PER_DIEM_MILEAGE_RATE_EXPENSE_TYPE_CODE, KFSConstants.EMPTY_STRING);
306 
307         // Check for changes on trip begin and trip end.
308         // This is necessary to prevent duplication of per diem creation due to timestamp changes.
309         boolean datesChanged = false;
310         if (perDiemExpenseList != null && !perDiemExpenseList.isEmpty()) {
311             Timestamp tempStart = perDiemExpenseList.get(0).getMileageDate();
312             Timestamp tempEnd = perDiemExpenseList.get(0).getMileageDate();
313 
314             if (perDiemExpenseList.size() > 1) {
315                 tempEnd = perDiemExpenseList.get(perDiemExpenseList.size()-1).getMileageDate();
316             }
317 
318             if (!(tempStart.equals(start) && tempEnd.equals(end))) {
319                 // the perDiemExpenseList will be cleared once we recreate the table, but we need it for carrying over mileage rates
320                 datesChanged = true;
321             }
322         }
323 
324         List<PerDiem> perDiemList = new ArrayList<PerDiem>();
325 
326         // find a valid per diem for each date.  If per diem is null, make it a custom per diem.
327         for (final Timestamp eachTimestamp : dateRange(start, end)) {
328             PerDiem perDiem = getPerDiemService().getPerDiem(document.getPrimaryDestinationId(), eachTimestamp, document.getEffectiveDateForPerDiem(eachTimestamp));
329             if (perDiem == null || perDiem.getPrimaryDestinationId() == TemConstants.CUSTOM_PRIMARY_DESTINATION_ID){
330                 perDiem = new PerDiem();
331                 perDiem.setPrimaryDestination(new PrimaryDestination());
332                 perDiem.getPrimaryDestination().setRegion(new TemRegion());
333                 perDiem.getPrimaryDestination().getRegion().setTripType(new TripType());
334                 perDiem.setPrimaryDestinationId(TemConstants.CUSTOM_PRIMARY_DESTINATION_ID);
335                 perDiem.getPrimaryDestination().getRegion().setRegionName(document.getPrimaryDestinationCountryState());
336                 perDiem.getPrimaryDestination().setCounty(document.getPrimaryDestinationCounty());
337                 perDiem.getPrimaryDestination().getRegion().setTripType(document.getTripType());
338                 perDiem.getPrimaryDestination().getRegion().setTripTypeCode(document.getTripTypeCode());
339                 perDiem.getPrimaryDestination().setPrimaryDestinationName(document.getPrimaryDestinationName());
340             }
341             perDiemList.add(perDiem);
342         }
343 
344         final Map<Timestamp, PerDiemExpense> perDiemMapped = new HashMap<Timestamp, PerDiemExpense>();
345 
346         int diffStartDays = 0;
347         if (perDiemExpenseList.size() > 0 && perDiemExpenseList.get(0).getMileageDate() != null && !datesChanged) {
348             diffStartDays = dateTimeService.dateDiff(start, perDiemExpenseList.get(0).getMileageDate(), false);
349         }
350 
351         Calendar endCal = Calendar.getInstance();
352 
353         if (end != null) {
354             endCal.setTime(end);
355             if (!datesChanged) {
356                 for (final PerDiemExpense perDiemItem : perDiemExpenseList) {
357                     if (diffStartDays != 0) {
358                         Calendar cal = Calendar.getInstance();
359                         cal.setTime(perDiemItem.getMileageDate());
360                         cal.add(Calendar.DATE, -diffStartDays);
361                         perDiemItem.setMileageDate(new Timestamp(cal.getTimeInMillis()));
362                     }
363 
364                     if (perDiemItem.getMileageDate() != null) {
365                         Calendar currCal = Calendar.getInstance();
366                         currCal.setTime(perDiemItem.getMileageDate());
367                         if (!endCal.before(currCal)) {
368                             perDiemMapped.put(perDiemItem.getMileageDate(), perDiemItem);
369                         }
370                     }
371                 }
372             }
373 
374             LOG.debug("Iterating over date range from "+ start+ " to "+ end);
375             int counter = 0;
376             for (final Timestamp someDate : dateRange(start, end)) {
377                 // Check if a per diem entry exists for this date
378                 if (!perDiemMapped.containsKey(someDate)) {
379                     final boolean prorated = shouldProrate(someDate, start, end);
380                     PerDiemExpense perDiemExpense = createPerDiemItem(document,perDiemList.get(counter), someDate, prorated, mileageRateExpenseTypeCode);
381                     perDiemExpense.setDocumentNumber(document.getDocumentNumber());
382                     perDiemMapped.put(someDate, perDiemExpense);
383                 }
384                 counter++;
385             }
386         }
387 
388         // Sort the dates and recreate the collection
389         perDiemExpenseList.clear();
390         for (final Timestamp someDate : new TreeSet<Timestamp>(perDiemMapped.keySet())) {
391             LOG.debug("Adding "+ perDiemMapped.get(someDate)+ " to perdiem list");
392             perDiemExpenseList.add(perDiemMapped.get(someDate));
393         }
394     }
395 
396     /**
397      * Determines if per diem expenses on the given date should be prorated
398      * @param perDiemDate the timestamp of the per diem
399      * @param tripBegin the begin timestamp of the trip
400      * @param tripEnd the end timestamp of the trip
401      * @return true if the per diem expense should be prorated, false otherwise
402      */
403     protected boolean shouldProrate(Timestamp perDiemDate, Timestamp tripBegin, Timestamp tripEnd) {
404         final boolean prorated = !KfsDateUtils.isSameDay(tripBegin, tripEnd) && (KfsDateUtils.isSameDay(perDiemDate, tripBegin) || KfsDateUtils.isSameDay(perDiemDate, tripEnd));
405         return prorated;
406     }
407 
408     /**
409      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getMileageRateKeyValues(java.sql.Date)
410      */
411     @Override
412     public List<KeyValue> getMileageRateKeyValues(Date searchDate) {
413         List<KeyValue> keyValues = new ArrayList<KeyValue>();
414 
415         TravelDocument document = (TravelDocument) ((TravelFormBase)KNSGlobalVariables.getKualiForm()).getDocument();
416         String documentType = getDocumentType(document);
417         final String travelerType = ObjectUtils.isNull(document.getTraveler()) ? null : document.getTraveler().getTravelerTypeCode();
418 
419         final Collection<ExpenseType> expenseTypes = getTravelExpenseService().getExpenseTypesForDocument(documentType, document.getTripTypeCode(), travelerType, false);
420 
421         for (final ExpenseType expenseType : expenseTypes) {
422             if (TemConstants.ExpenseTypeMetaCategory.MILEAGE.getCode().equals(expenseType.getExpenseTypeMetaCategoryCode())) {
423                 final MileageRate mileageRate = getMileageRateService().findMileageRateByExpenseTypeCodeAndDate(expenseType.getCode(), searchDate);
424                 if (mileageRate != null) {
425                     keyValues.add(new ConcreteKeyValue(expenseType.getCode(), expenseType.getCode()+" - "+mileageRate.getRate().toString()));
426                 }
427             }
428         }
429 
430         //sort by label
431         Comparator<KeyValue> labelComparator = new Comparator<KeyValue>() {
432             @Override
433             public int compare(KeyValue o1, KeyValue o2) {
434                 return o1.getKey().compareTo(o2.getKey());
435             }
436         };
437 
438         Collections.sort(keyValues, labelComparator);
439 
440         return keyValues;
441     }
442 
443     /**
444      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copyDownPerDiemExpense(int, java.util.List)
445      */
446     @Override
447     public void copyDownPerDiemExpense(TravelDocument travelDocument, int copyIndex, List<PerDiemExpense> perDiemExpenses) {
448 
449         PerDiemExpense lineToCopy = perDiemExpenses.get(copyIndex);
450         PerDiemExpense restoredLine = getRestoredPerDiemForCopying(travelDocument, lineToCopy);
451 
452         List<PerDiemExpense> tempPerDiemExpenses = new ArrayList<PerDiemExpense>();
453 
454         if (copyIndex < perDiemExpenses.size()) {
455             for (int i = 0; i < perDiemExpenses.size(); i++) {
456                 PerDiemExpense perDiemExpense = new PerDiemExpense();
457                 if (perDiemExpenses != null && i < copyIndex) {
458                     // copy over from the old list
459                     perDiemExpense = perDiemExpenses.get(i);
460                 }
461                 else if (i > copyIndex) {
462                     perDiemExpense = copyPerDiemExpense(restoredLine);
463                     perDiemExpense.setMileageDate(perDiemExpenses.get(i).getMileageDate());
464                     if (shouldProrate(perDiemExpense.getMileageDate(), travelDocument.getTripBegin(), travelDocument.getTripEnd())) {
465                         // prorate
466                         perDiemExpense.setProrated(true);
467                         if (perDiemExpense.getPrimaryDestinationId() == TemConstants.CUSTOM_PRIMARY_DESTINATION_ID) {
468                             // prorate the restored line to create new per diem
469                             final PerDiem perDiem = copyIntoPerDiem(restoredLine);
470                             final Integer perDiemPercent = lookupProratePercentage(perDiemExpense, travelDocument.getTripType().getPerDiemCalcMethod(), travelDocument.getTripEnd());
471                             perDiemExpense.setDinnerValue(PerDiemExpense.calculateMealsAndIncidentalsProrated(perDiemExpense.getDinnerValue(), perDiemPercent));
472                             perDiemExpense.setLunchValue(PerDiemExpense.calculateMealsAndIncidentalsProrated(perDiemExpense.getLunchValue(), perDiemPercent));
473                             perDiemExpense.setBreakfastValue(PerDiemExpense.calculateMealsAndIncidentalsProrated(perDiemExpense.getBreakfastValue(), perDiemPercent));
474                             perDiemExpense.setIncidentalsValue(PerDiemExpense.calculateMealsAndIncidentalsProrated(perDiemExpense.getIncidentalsValue(), perDiemPercent));
475                         } else {
476                             final PerDiem perDiem = getPerDiemService().getPerDiem(restoredLine.getPrimaryDestinationId(), perDiemExpense.getMileageDate(), travelDocument.getEffectiveDateForPerDiem(perDiemExpense));
477                             setPerDiemMealsAndIncidentals(perDiemExpense, perDiem, travelDocument.getTripType(), travelDocument.getTripEnd(), true);
478                         }
479                     }
480                     if (travelDocument.getTripEnd() != null && KfsDateUtils.isSameDay(travelDocument.getTripEnd(), perDiemExpense.getMileageDate())) {
481                         // set lodging to 0
482                         perDiemExpense.setLodging(KualiDecimal.ZERO);
483                     }
484                 }
485                 else {
486                     // are we copying a prorated line to a non-prorated spot?
487 
488                     // then let's restore all values before copying
489                     perDiemExpense = lineToCopy;
490                 }
491 
492                 tempPerDiemExpenses.add(perDiemExpense);
493 
494             }
495         }
496 
497         perDiemExpenses.clear();
498         perDiemExpenses.addAll(tempPerDiemExpenses);
499     }
500 
501     /**
502      * If the given perDiemExpense was prorated, restores the original values
503      * @param travelDocument the travel document the expense is on
504      * @param perDiemExpense the per diem expense to restore
505      * @return a PerDiemExpense with all values restored
506      */
507     protected PerDiemExpense getRestoredPerDiemForCopying(TravelDocument travelDocument, PerDiemExpense perDiemExpense) {
508         PerDiemExpense restoredExpense = copyPerDiemExpense(perDiemExpense);
509         if (travelDocument.getPrimaryDestinationId() == TemConstants.CUSTOM_PRIMARY_DESTINATION_ID && shouldProrate(perDiemExpense.getMileageDate(), travelDocument.getTripBegin(), travelDocument.getTripEnd())) {
510             final Integer perDiemPercentage = lookupProratePercentage(perDiemExpense, travelDocument.getTripType().getPerDiemCalcMethod(), travelDocument.getTripEnd());
511             if (perDiemPercentage != null) {
512                 final KualiDecimal perDiemPercentageDecimal = new KualiDecimal((double)perDiemPercentage*0.01);
513                 restoredExpense.setBreakfastValue(perDiemExpense.getBreakfastValue().divide(perDiemPercentageDecimal));
514                 restoredExpense.setLunchValue(perDiemExpense.getLunchValue().divide(perDiemPercentageDecimal));
515                 restoredExpense.setDinnerValue(perDiemExpense.getDinnerValue().divide(perDiemPercentageDecimal));
516                 restoredExpense.setIncidentalsValue(perDiemExpense.getIncidentalsValue().divide(perDiemPercentageDecimal));
517             }
518             perDiemExpense.setProrated(false);
519         } else {
520             // look up per diem
521             final PerDiem perDiem = getPerDiemService().getPerDiem(perDiemExpense.getPrimaryDestinationId(), perDiemExpense.getMileageDate(), travelDocument.getEffectiveDateForPerDiem(perDiemExpense));
522             setPerDiemMealsAndIncidentals(restoredExpense, perDiem, travelDocument.getTripType(), travelDocument.getTripEnd(), false);
523         }
524         return restoredExpense;
525     }
526 
527     /**
528      * Takes the values from the given per diem expense and copies them into a per diem
529      * @param perDiemExpense the per diem expense to copy values from
530      * @return a fake PerDiem record copied from those values
531      */
532     protected PerDiem copyIntoPerDiem(PerDiemExpense perDiemExpense) {
533         PerDiem perDiem = new PerDiem();
534         perDiem.setPrimaryDestinationId(perDiemExpense.getPrimaryDestinationId());
535         perDiem.setBreakfast(perDiemExpense.getBreakfastValue());
536         perDiem.setLunch(perDiemExpense.getLunchValue());
537         perDiem.setDinner(perDiemExpense.getDinnerValue());
538         perDiem.setIncidentals(perDiemExpense.getIncidentalsValue());
539         return perDiem;
540     }
541 
542     /**
543      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getDocumentsRelatedTo(org.kuali.kfs.module.tem.document.TravelDocument)
544      */
545     @Override
546     public Map<String, List<Document>> getDocumentsRelatedTo(final TravelDocument document) throws WorkflowException {
547         return getDocumentsRelatedTo(document.getDocumentNumber());
548     }
549 
550     /**
551      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getDocumentsRelatedTo(java.lang.String)
552      */
553     @Override
554     public Map<String, List<Document>> getDocumentsRelatedTo(final String documentNumber) throws WorkflowException {
555         final Map<String, List<Document>> retval = new HashMap<String, List<Document>>();
556 
557         Set<String> documentNumbers = accountingDocumentRelationshipService.getAllRelatedDocumentNumbers(documentNumber);
558         if (!documentNumbers.isEmpty()) {
559             for (String documentHeaderId : documentNumbers) {
560                 Document doc = documentService.getByDocumentHeaderIdSessionless(documentHeaderId);
561                 if (doc != null) {
562                     Class<? extends Document> clazz = doc.getClass();
563 
564                     if (clazz != null) {
565                         String docTypeName = getDataDictionaryService().getDocumentTypeNameByClass(clazz);
566 
567                         List<Document> docs = retval.get(docTypeName);
568                         if (docs == null) {
569                             docs = new ArrayList<Document>();
570                         }
571                         docs.add(doc);
572 
573                         retval.put(docTypeName, docs);
574                     }
575                 }
576             }
577         }
578 
579         return retval;
580     }
581 
582     /**
583      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getDocumentsRelatedTo(org.kuali.kfs.module.tem.document.TravelDocument, java.lang.String[])
584      */
585     @Override
586     public List<Document> getDocumentsRelatedTo(final TravelDocument document, String... documentTypeList){
587         List<Document> relatedDocumentList = new ArrayList<Document>();
588         Map<String, List<Document>> relatedDocumentMap;
589         try {
590             relatedDocumentMap = getDocumentsRelatedTo(document);
591             for (String documentType : documentTypeList){
592                 if (relatedDocumentMap.containsKey(documentType)){
593                     relatedDocumentList.addAll(relatedDocumentMap.get(documentType));
594                 }
595             }
596         }
597         catch (WorkflowException ex) {
598             LOG.error(ex.getMessage(), ex);
599             throw new RuntimeException(ex);
600         }
601         return relatedDocumentList;
602     }
603 
604     @Override
605     public List<SpecialCircumstances> findActiveSpecialCircumstances(String documentNumber, String documentType) {
606         List<SpecialCircumstances> retval = new ArrayList<SpecialCircumstances>();
607         Map<String, Object> criteria = new HashMap<String, Object>();
608         criteria.put(KFSPropertyConstants.ACTIVE, true);
609 
610         // add specialCircumstances with specific documentType SpecialCircumstancesQuestion
611         Set<String> documentTypesToCheck = new HashSet<String>();
612         documentTypesToCheck.add(documentType);
613         final Set<String> parentDocTypes = getTravelService().getParentDocumentTypeNames(documentType);
614         documentTypesToCheck.addAll(parentDocTypes);
615         criteria.put(KFSPropertyConstants.DOCUMENT_TYPE, documentTypesToCheck);
616         retval.addAll(buildSpecialCircumstances(documentNumber, criteria));
617 
618         return retval;
619     }
620 
621 
622     protected List<SpecialCircumstances> buildSpecialCircumstances(String documentNumber, Map<String, Object> criteria) {
623         List<SpecialCircumstances> retval = new ArrayList<SpecialCircumstances>();
624 
625         Collection<SpecialCircumstancesQuestion> questions = getBusinessObjectService().findMatching(SpecialCircumstancesQuestion.class, criteria);
626         for (SpecialCircumstancesQuestion question : questions) {
627             SpecialCircumstances spc = new SpecialCircumstances();
628             spc.setDocumentNumber(documentNumber);
629             spc.setQuestionId(question.getId());
630             spc.setQuestion(question);
631             retval.add(spc);
632         }
633 
634         return retval;
635     }
636 
637     /**
638      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#findAuthorizationDocuments(java.lang.String)
639      */
640     @Override
641     public List<TravelAuthorizationDocument> findAuthorizationDocuments(final String travelDocumentIdentifier){
642         final List<String> ids = findAuthorizationDocumentNumbers(travelDocumentIdentifier);
643 
644         List<TravelAuthorizationDocument> resultDocumentLists = new ArrayList<TravelAuthorizationDocument>();
645         //retrieve the actual documents
646         try {
647             if (!ids.isEmpty()) {
648                 for (Document document : getDocumentService().getDocumentsByListOfDocumentHeaderIds(TravelAuthorizationDocument.class, ids)){
649                     resultDocumentLists.add((TravelAuthorizationDocument)document);
650                 }
651             }
652         }catch (WorkflowException wfe){
653             LOG.error(wfe.getMessage(), wfe);
654         }
655         return resultDocumentLists;
656     }
657 
658     /**
659      * Gets the document numbers from the TravelDocumentDao for the given trip id
660      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#findAuthorizationDocumentNumbers(java.lang.String)
661      */
662     @Override
663     public List<String> findAuthorizationDocumentNumbers(final String travelDocumentIdentifier) {
664         final List<String> ids = getTravelDocumentDao().findDocumentNumbers(TravelAuthorizationDocument.class, travelDocumentIdentifier);
665         return ids;
666     }
667 
668     /**
669      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#findReimbursementDocuments(java.lang.String)
670      */
671     @Override
672     public List<TravelReimbursementDocument> findReimbursementDocuments(final String travelDocumentIdentifier) {
673         final List<String> ids = getTravelDocumentDao().findDocumentNumbers(TravelReimbursementDocument.class, travelDocumentIdentifier);
674 
675         List<TravelReimbursementDocument> resultDocumentLists = new ArrayList<TravelReimbursementDocument>();
676         // retrieve the actual documents
677         try {
678             if (!ids.isEmpty()) {
679                 for (Document document : getDocumentService().getDocumentsByListOfDocumentHeaderIds(TravelReimbursementDocument.class, ids)) {
680                     resultDocumentLists.add((TravelReimbursementDocument) document);
681                 }
682             }
683         }
684         catch (WorkflowException wfe) {
685             throw new RuntimeException(wfe);
686         }
687         return resultDocumentLists;
688     }
689 
690 
691     /**
692      *
693      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#addAdHocFYIRecipient(org.kuali.rice.kns.document.Document)
694      */
695     @Override
696     public void addAdHocFYIRecipient(final Document document) {
697         addAdHocFYIRecipient(document, document.getDocumentHeader().getWorkflowDocument().getInitiatorPrincipalId());
698     }
699 
700     /**
701      *
702      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#addAdHocFYIRecipient(org.kuali.rice.kns.document.Document, java.lang.String)
703      */
704     @Override
705     public void addAdHocFYIRecipient(final Document document, String initiatorUserId) {
706         addAdHocRecipient(document, initiatorUserId, KewApiConstants.ACTION_REQUEST_FYI_REQ);
707     }
708 
709     /**
710      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#addAdHocRecipient(Document, String, String)
711      */
712     @Override
713     public void addAdHocRecipient(Document document, String initiatorUserId, String actionRequested) {
714         List<AdHocRoutePerson> adHocRoutePersons = document.getAdHocRoutePersons();
715         List<String> adHocRoutePersonIds = new ArrayList<String>();
716         if(!adHocRoutePersons.isEmpty()){
717             for(AdHocRoutePerson ahrp : adHocRoutePersons){
718                 adHocRoutePersonIds.add(ahrp.getId());
719             }
720         }
721 
722         // Add adhoc for initiator
723         if (!adHocRoutePersonIds.contains(initiatorUserId)) {
724             if (initiatorUserId != null) {
725                 final Person finSysUser = SpringContext.getBean(PersonService.class).getPerson(initiatorUserId);
726                 if (finSysUser != null) {
727                     final AdHocRoutePerson recipient = buildAdHocRecipient(finSysUser.getPrincipalName(), actionRequested);
728                     final DocumentAuthorizer documentAuthorizer = SpringContext.getBean(DocumentHelperService.class).getDocumentAuthorizer(document);
729                     if (documentAuthorizer.canReceiveAdHoc(document, finSysUser, actionRequested)) {
730                         adHocRoutePersons.add(recipient);
731                     }
732                 }
733                 else {
734                     LOG.warn("finSysUser is null.");
735                 }
736             }
737             else {
738                 LOG.warn("initiatorUserId is null.");
739             }
740         }
741     }
742 
743     /**
744      * This method builds the AdHoc Route Person
745      *
746      * @param userId
747      * @return
748      */
749     protected AdHocRoutePerson buildAdHocRecipient(String userId, String actionRequested) {
750         AdHocRoutePerson adHocRoutePerson = new AdHocRoutePerson();
751         adHocRoutePerson.setActionRequested(actionRequested);
752         adHocRoutePerson.setId(userId);
753         return adHocRoutePerson;
754     }
755 
756     /**
757      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#calculateDailyTotals(java.util.List)
758      */
759     @Override
760     public List<Map<String, KualiDecimal>> calculateDailyTotals(List<PerDiemExpense> perDiemExpenses) {
761         List<Map<String, KualiDecimal>> tripTotals = new ArrayList<Map<String, KualiDecimal>>();
762 
763         for (PerDiemExpense perDiemExpense : perDiemExpenses){
764             Map<String, KualiDecimal> dailyTotal = calculateDailyTotal(perDiemExpense);
765             tripTotals.add(dailyTotal);
766         }
767 
768         return tripTotals;
769     }
770 
771     /**
772      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#calculateDailyTotal(org.kuali.kfs.module.tem.businessobject.PerDiemExpense,
773      *      boolean)
774      */
775     @Override
776     public Map<String, KualiDecimal> calculateDailyTotal(PerDiemExpense perDiemExpense) {
777         Map<String, KualiDecimal> dailyTotals = new HashMap<String, KualiDecimal>();
778 
779         dailyTotals.put(TemConstants.MILEAGE_TOTAL_ATTRIBUTE, perDiemExpense.getMileageTotal());
780         dailyTotals.put(TemConstants.LODGING_TOTAL_ATTRIBUTE, perDiemExpense.getLodgingTotal());
781         dailyTotals.put(TemConstants.MEALS_AND_INC_TOTAL_ATTRIBUTE, perDiemExpense.getMealsAndIncidentals());
782         dailyTotals.put(TemConstants.DAILY_TOTAL, perDiemExpense.getDailyTotal());
783 
784         return dailyTotals;
785     }
786 
787     @Override
788     public void routeToFiscalOfficer(final TravelDocument document, final String noteText) throws WorkflowException, Exception {
789         // Below used as a place holder to allow code to specify actionForward to return if not a 'success question'
790         final Note newNote = getDocumentService().createNoteFromDocument(document, noteText);
791         document.addNote(newNote);
792         getNoteService().save(newNote);
793 
794         final WorkflowDocument workflowDocument = document.getDocumentHeader().getWorkflowDocument();
795         workflowDocument.returnToPreviousNode(noteText, KFSConstants.RouteLevelNames.ACCOUNT);
796 
797         final String messagePattern = configurationService.getPropertyValueAsString(TemKeyConstants.MESSAGE_DOCUMENT_TEM_RETURNED_TO_FISCAL_OFFICER);
798         final String annotation = MessageFormat.format(messagePattern, GlobalVariables.getUserSession().getPerson().getPrincipalName());
799 
800         workflowDocument.adHocToPrincipal( ActionRequestType.FYI, KFSConstants.RouteLevelNames.ACCOUNT, annotation, workflowDocument.getInitiatorPrincipalId(), TemConstants.INITIATOR_RESPONSIBILITY, true);
801 
802         document.refreshReferenceObject(KFSPropertyConstants.DOCUMENT_HEADER);
803 
804         document.getFinancialSystemDocumentHeader().updateAndSaveAppDocStatus(TemConstants.TravelStatusCodeKeys.AWAIT_FISCAL);
805     }
806 
807     /**
808      *
809      * This method calculates the prorate percentage value based on perDiemCalcMethod (P/Q)
810      * @param expense
811      * @param perDiemCalcMethod
812      * @return
813      */
814     @Override
815     public Integer calculateProratePercentage(PerDiemExpense perDiemExpense, String perDiemCalcMethod, Timestamp tripEnd) {
816         Integer perDiemPercent = 100;
817 
818         if (perDiemExpense.isProrated()) {
819             perDiemPercent = lookupProratePercentage(perDiemExpense, perDiemCalcMethod, tripEnd);
820         }
821         return perDiemPercent;
822     }
823 
824     /**
825      * Looks up the prorate percentage, even if the per diem doesn't think it's prorated
826      * @param perDiemExpense the per diem expense to find a percentage for (if the quarterly method is used)
827      * @param perDiemCalcMethod the per diem calculation method
828      * @param tripEnd the last day of the trip (used for the quarterly method)
829      * @return a prorate percentage, or 100 if nothing could be found
830      */
831     protected Integer lookupProratePercentage(PerDiemExpense perDiemExpense, String perDiemCalcMethod, Timestamp tripEnd) {
832         if (perDiemCalcMethod != null && perDiemCalcMethod.equals(TemConstants.PERCENTAGE)) {
833             try {
834                 final String perDiemPercentage = parameterService.getParameterValueAsString(TravelAuthorizationDocument.class, TravelAuthorizationParameters.FIRST_AND_LAST_DAY_PER_DIEM_PERCENTAGE, "100");
835                 final Integer perDiemPercent = Integer.parseInt(perDiemPercentage);
836                 return perDiemPercent;
837             }
838             catch (Exception e1) {
839                 LOG.error("Failed to process prorate percentage for FIRST_AND_LAST_DAY_PER_DIEM_PERCENTAGE parameter.", e1);
840             }
841         }
842         else {
843             return calculatePerDiemPercentageFromTimestamp(perDiemExpense, tripEnd);
844         }
845         return 100;
846     }
847 
848     @Override
849     public Integer calculatePerDiemPercentageFromTimestamp(PerDiemExpense perDiemExpense, Timestamp tripEnd) {
850         if (perDiemExpense.getMileageDate() != null) {
851             try {
852                 Collection<String> quarterTimes = parameterService.getParameterValuesAsString(TemParameterConstants.TEM_DOCUMENT.class, TravelParameters.QUARTER_DAY_TIME_TABLE);
853 
854                 // Take date and compare to the quadrant specified.
855                 Calendar prorateDate = new GregorianCalendar();
856                 prorateDate.setTime(perDiemExpense.getMileageDate());
857 
858                 int quadrantIndex = 4;
859                 for (String qT : quarterTimes) {
860                     String[] indexTime = qT.split("=");
861                     String[] hourMinute = indexTime[1].split(":");
862 
863                     Calendar qtCal = new GregorianCalendar();
864                     qtCal.setTime(perDiemExpense.getMileageDate());
865                     qtCal.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hourMinute[0]));
866                     qtCal.set(Calendar.MINUTE, Integer.parseInt(hourMinute[1]));
867 
868                     if (prorateDate.equals(qtCal) || prorateDate.before(qtCal)) {
869                         quadrantIndex = Integer.parseInt(indexTime[0]);
870                         break;
871                     }
872                 }
873 
874                 // Prorate on trip begin. (12:01 AM arrival = 100%, 11:59 PM arrival = 25%)
875                 if (tripEnd != null && !KfsDateUtils.isSameDay(prorateDate.getTime(), tripEnd)) {
876                     return 100 - ((quadrantIndex - 1) * TemConstants.QUADRANT_PERCENT_VALUE);
877                 }
878                 else { // Prorate on trip end. (12:01 AM departure = 25%, 11:59 PM arrival = 100%).
879                     return quadrantIndex * TemConstants.QUADRANT_PERCENT_VALUE;
880                 }
881             }
882             catch (IllegalArgumentException e2) {
883                 LOG.error("IllegalArgumentException.", e2);
884             }
885         }
886 
887         return 100;
888     }
889 
890     /**
891      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#transferPerDiemMileage(org.kuali.kfs.module.tem.businessobject.PerDiemMileage)
892      */
893     @Override
894     public PerDiemExpense copyPerDiemExpense(PerDiemExpense perDiemExpense) {
895         final PerDiemExpense retval = new PerDiemExpense();
896         retval.setDocumentNumber(perDiemExpense.getDocumentNumber());
897 
898         retval.setCountryState(perDiemExpense.getCountryState());
899         retval.setCounty(perDiemExpense.getCounty());
900         retval.setPrimaryDestination(perDiemExpense.getPrimaryDestination());
901         retval.setMileageDate(perDiemExpense.getMileageDate());
902         retval.setMiles(perDiemExpense.getMiles());
903         retval.setMileageRateExpenseTypeCode(perDiemExpense.getMileageRateExpenseTypeCode());
904         retval.setAccommodationTypeCode(perDiemExpense.getAccommodationTypeCode());
905         retval.setAccommodationName(perDiemExpense.getAccommodationName());
906         retval.setAccommodationPhoneNum(perDiemExpense.getAccommodationPhoneNum());
907         retval.setAccommodationAddress(perDiemExpense.getAccommodationAddress());
908         retval.setPrimaryDestinationId(perDiemExpense.getPrimaryDestinationId());
909 
910         if (retval.getMiles() == null) {
911             retval.setMiles(0);
912         }
913 
914         if (perDiemExpense.getLodging() == null || perDiemExpense.getLodging().isNegative()) {
915             retval.setLodging(KualiDecimal.ZERO);
916         }
917         else {
918             retval.setLodging(perDiemExpense.getLodging());
919         }
920 
921         retval.setPersonal(perDiemExpense.getPersonal());
922         retval.setBreakfast(perDiemExpense.getBreakfast());
923         retval.setLunch(perDiemExpense.getLunch());
924         retval.setDinner(perDiemExpense.getDinner());
925 
926         retval.setBreakfastValue(perDiemExpense.getBreakfastValue());
927         retval.setLunchValue(perDiemExpense.getLunchValue());
928         retval.setDinnerValue(perDiemExpense.getDinnerValue());
929         retval.setIncidentalsValue(perDiemExpense.getIncidentalsValue());
930 
931         LOG.debug("estimated meals and incidentals "+ retval.getMealsAndIncidentals());
932 
933         return retval;
934     }
935 
936     @Override
937     /**
938      * Calculates Mileage and returns total mileage amount
939      * @param ActualExpense actualExpense
940      */
941     public KualiDecimal calculateMileage(ActualExpense actualExpense) {
942         KualiDecimal mileageTotal = KualiDecimal.ZERO;
943         if (ObjectUtils.isNotNull(actualExpense.getExpenseTypeCode()) && actualExpense.isMileage()) {
944             mileageTotal = actualExpense.getMileageTotal();
945         }
946         return mileageTotal;
947     }
948 
949 
950     protected ActualExpense getParentActualExpense(final List<ActualExpense> actualExpenses, Long expenseId) {
951         if (ObjectUtils.isNotNull(actualExpenses) && ObjectUtils.isNotNull(expenseId)) {
952 
953             for (final ActualExpense actualExpense : actualExpenses) {
954 
955                 if (actualExpense.getId().equals(expenseId)) {
956                     return actualExpense;
957                 }
958 
959             }
960         }
961 
962         return null;
963     }
964 
965     /**
966      *
967      */
968     @Override
969     public void handleNewActualExpense(final ActualExpense newActualExpenseLine) {
970         if (newActualExpenseLine.getExpenseAmount() != null) {
971             final BigDecimal rate = newActualExpenseLine.getCurrencyRate();
972             final KualiDecimal amount = newActualExpenseLine.getExpenseAmount();
973 
974             newActualExpenseLine.setConvertedAmount(new KualiDecimal(amount.bigDecimalValue().multiply(rate)));
975             LOG.debug("Set converted amount for "+ newActualExpenseLine+ " to "+ newActualExpenseLine.getConvertedAmount());
976 
977             if (isHostedMeal(newActualExpenseLine)) {
978                 KNSGlobalVariables.getMessageList().add(TemKeyConstants.MESSAGE_HOSTED_MEAL_ADDED,
979                         new SimpleDateFormat("MM/dd/yyyy").format(newActualExpenseLine.getExpenseDate()),
980                         newActualExpenseLine.getExpenseTypeObjectCode().getExpenseType().getName());
981                 newActualExpenseLine.setNonReimbursable(true);
982             }
983         }
984     }
985 
986     /**
987      * 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
988      * provided by a hosting institution and cannot be taken as a reimbursement. Uses the HOSTED_MEAL_EXPENSE_TYPES system parameter
989      * to check the expense type against
990      *
991      * @param havingExpenseType has an expense type to check for meal hosting
992      * @return true if the expense is a hosted meal or not
993      */
994     @Override
995     public boolean isHostedMeal(final ExpenseTypeAware havingExpenseType) {
996         if (ObjectUtils.isNull(havingExpenseType) || StringUtils.isBlank(havingExpenseType.getExpenseTypeCode())) {
997             return false;
998         }
999 
1000         if (havingExpenseType instanceof PersistableBusinessObject) {
1001             ((PersistableBusinessObject)havingExpenseType).refreshReferenceObject(TemPropertyConstants.EXPENSE_TYPE);
1002         }
1003         if (ObjectUtils.isNull(havingExpenseType.getExpenseType())) {
1004             return false;
1005         }
1006         return havingExpenseType.getExpenseType().isHosted();
1007     }
1008 
1009     /**
1010      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#isTravelManager(org.kuali.rice.kim.bo.Person)
1011      */
1012     @Override
1013     public boolean isTravelManager(final Person user) {
1014         return getTemRoleService().isTravelManager(user);
1015     }
1016 
1017     /**
1018      * Digs up a message from the {@link ConfigurationService} by key
1019      */
1020     @Override
1021     public String getMessageFrom(final String messageType, String... args) {
1022         String strTemp = getConfigurationService().getPropertyValueAsString(messageType);
1023         for(int i=0;i<args.length;i++){
1024             strTemp = strTemp.replaceAll("\\{"+i+"\\}", args[i]);
1025         }
1026         return strTemp;
1027     }
1028 
1029     /**
1030      * is this document in an open for reimbursement workflow state?
1031      *
1032      * @param reqForm
1033      * @return
1034      */
1035     @Override
1036     public boolean isOpen(TravelDocument document) {
1037         return document.getAppDocStatus().equals(TemConstants.TravelAuthorizationStatusCodeKeys.OPEN_REIMB);
1038     }
1039 
1040     /**
1041      * is this document in a final workflow state.
1042      *
1043      * @param reqForm
1044      * @return
1045      */
1046     @Override
1047     public boolean isFinal(TravelDocument document) {
1048         return document.getDocumentHeader().getWorkflowDocument().isFinal();
1049     }
1050 
1051     /**
1052      *
1053      * @param document
1054      * @return
1055      */
1056     @Override
1057     public boolean isTravelAuthorizationProcessed(TravelAuthorizationDocument document){
1058         return isFinal(document) || isProcessed(document);
1059     }
1060 
1061     /**
1062      *
1063      * @param document
1064      * @return
1065      */
1066     @Override
1067     public boolean isTravelAuthorizationOpened(TravelAuthorizationDocument document){
1068         return isTravelAuthorizationProcessed(document) && isOpen(document);
1069     }
1070 
1071     /**
1072      * is this document in a processed workflow state?
1073      *
1074      * @param reqForm
1075      * @return
1076      */
1077     @Override
1078     public boolean isProcessed(TravelDocument document) {
1079         return document.getDocumentHeader().getWorkflowDocument().isProcessed();
1080     }
1081 
1082     @Override
1083     public KualiDecimal getAmountDueFromInvoice(String documentNumber, KualiDecimal requestedAmount) {
1084         try {
1085             AccountsReceivableCustomerInvoice doc = (AccountsReceivableCustomerInvoice) documentService.getByDocumentHeaderId(documentNumber);
1086             if (doc != null) {
1087                 return doc.getOpenAmount();
1088             }
1089         }
1090         catch (WorkflowException we) {
1091             throw new RuntimeException(we);
1092         }
1093 
1094         return requestedAmount;
1095     }
1096 
1097     /**
1098      * Find the current travel authorization.  This includes any amendments.
1099      *
1100      * @param trDocument
1101      * @return
1102      * @throws WorkflowException
1103      */
1104     @Override
1105     public TravelAuthorizationDocument findCurrentTravelAuthorization(TravelDocument document) {
1106 
1107         TravelAuthorizationDocument travelDocument = null;
1108 
1109         try {
1110             final Map<String, List<Document>> relatedDocuments = getDocumentsRelatedTo(document);
1111             List<Document> taDocs  = relatedDocuments.get(TravelDocTypes.TRAVEL_AUTHORIZATION_DOCUMENT);
1112             List<Document> taaDocs = relatedDocuments.get(TravelDocTypes.TRAVEL_AUTHORIZATION_AMEND_DOCUMENT);
1113             List<Document> tacDocs = relatedDocuments.get(TravelDocTypes.TRAVEL_AUTHORIZATION_CLOSE_DOCUMENT);
1114 
1115             //If TAC exists, it will always be the most current travel auth doc
1116             if (tacDocs != null && !tacDocs.isEmpty()) {
1117                 travelDocument =  (TravelAuthorizationDocument) tacDocs.get(0);
1118             }
1119             //find the TAA with the correct status
1120             else if (taaDocs != null && !taaDocs.isEmpty()){
1121                 for (Document tempDocument : taaDocs){
1122                     //Find the doc that is the open to perform actions against.
1123                     if (isTravelAuthorizationOpened((TravelAuthorizationDocument)tempDocument)){
1124                         travelDocument = (TravelAuthorizationDocument) tempDocument;
1125                     }
1126                 }
1127             }
1128             //return TA doc if no amendments exist
1129             if (travelDocument == null) {
1130                 //if the taDocs is null, initialize an empty list
1131                 taDocs = taDocs == null? new ArrayList<Document>() : taDocs;
1132 
1133                 if (taDocs.isEmpty()) {
1134                     //this should find the TA document for sure
1135                     final List<TravelAuthorizationDocument> tempTaDocs = findAuthorizationDocuments(document.getTravelDocumentIdentifier());
1136                     if (!tempTaDocs.isEmpty()){
1137                         travelDocument = tempTaDocs.get(0);
1138                     }
1139                 }else{
1140                     travelDocument = (TravelAuthorizationDocument) taDocs.get(0);
1141                 }
1142             }
1143         }
1144         catch (WorkflowException we) {
1145             final String docNum = (document != null && !StringUtils.isBlank(document.getDocumentNumber())) ? document.getDocumentNumber() : "???";
1146             throw new RuntimeException("Could not find documents related to document #"+docNum);
1147         }
1148         return travelDocument;
1149     }
1150 
1151     /**
1152      * Find the root document for creating a travel reimbursement from a previous document.
1153      *
1154      * @param trDocument
1155      * @return
1156      * @throws WorkflowException
1157      */
1158     @Override
1159     public TravelDocument findRootForTravelReimbursement(String travelDocumentIdentifier) {
1160 
1161         TravelDocument rootTravelDocument = null;
1162 
1163         try {
1164             //look for a current authorization first
1165 
1166             //use the travelDocumentIdentifier to find any saved authorization
1167             final Collection<TravelAuthorizationDocument> tempTaDocs = getTravelAuthorizationService().find(travelDocumentIdentifier);
1168 
1169             if (!tempTaDocs.isEmpty()) {
1170                 TravelAuthorizationDocument taDoc = null;
1171                 for(TravelAuthorizationDocument tempTaDoc : tempTaDocs) {
1172                     taDoc = tempTaDoc;
1173                     break;
1174                 }
1175 
1176                 //find the current travel authorization
1177                 rootTravelDocument = findCurrentTravelAuthorization(taDoc);
1178             }
1179 
1180             //no authorizations exist so the root should be a reimbursement
1181             else {
1182                 final List<TravelReimbursementDocument> tempTrDocs = findReimbursementDocuments(travelDocumentIdentifier);
1183                 //did not find any reimbursements either
1184                 if (tempTrDocs.isEmpty()) {
1185                     LOG.debug("Did not find any authorizations or reimbursements for travelDocumentIndentifier: "+ travelDocumentIdentifier);
1186                     return null;
1187                 }
1188 
1189                 //if there is only one document then that is the root
1190                 if (tempTrDocs.size() == 1) {
1191                     rootTravelDocument = tempTrDocs.get(0);
1192                 }
1193                 else {
1194                     //the root document can be found using any document in the list; just use the first one
1195                     String rootDocumentNumber = getAccountingDocumentRelationshipService().getRootDocumentNumber(tempTrDocs.get(0).getDocumentNumber());
1196                     TravelDocument tempDoc = (TravelDocument)documentService.getByDocumentHeaderIdSessionless(rootDocumentNumber);
1197 
1198                     rootTravelDocument = tempDoc;
1199                 }
1200             }
1201         }
1202         catch (WorkflowException we) {
1203             throw new RuntimeException("Could not find authorization or reimbursement documents related to trip id #"+travelDocumentIdentifier);
1204         }
1205 
1206         return rootTravelDocument;
1207     }
1208 
1209     @Override
1210     public KualiDecimal getTotalCumulativeReimbursements(TravelDocument document) {
1211         KualiDecimal trTotal = KualiDecimal.ZERO;
1212 
1213         List<Document> relatedTravelReimbursementDocuments = getDocumentsRelatedTo(document, TravelDocTypes.TRAVEL_REIMBURSEMENT_DOCUMENT);
1214         for(Document trDoc: relatedTravelReimbursementDocuments) {
1215             final TravelReimbursementDocument tr = (TravelReimbursementDocument)trDoc;
1216             if (!KFSConstants.DocumentStatusCodes.CANCELLED.equals(tr.getFinancialSystemDocumentHeader().getFinancialDocumentStatusCode()) && !KFSConstants.DocumentStatusCodes.DISAPPROVED.equals(tr.getFinancialSystemDocumentHeader().getFinancialDocumentStatusCode())) {
1217                 List<AccountingLine> lines = tr.getSourceAccountingLines();
1218                 for(AccountingLine line: lines) {
1219                     trTotal = trTotal.add(line.getAmount());
1220                 }
1221             }
1222         }
1223 
1224         if (document.getDocumentHeader().getWorkflowDocument().getDocumentTypeName().equals(TravelDocTypes.TRAVEL_REIMBURSEMENT_DOCUMENT)){
1225             List<AccountingLine> lines = document.getSourceAccountingLines();
1226             for(AccountingLine line: lines) {
1227                 trTotal = trTotal.add(line.getAmount());
1228             }
1229         }
1230 
1231         return trTotal;
1232     }
1233 
1234     /**
1235      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getTotalAuthorizedEncumbrance(org.kuali.kfs.module.tem.document.TravelDocument)
1236      */
1237     @Override
1238     public KualiDecimal getTotalAuthorizedEncumbrance(TravelDocument document) {
1239         KualiDecimal taTotal = KualiDecimal.ZERO;
1240         TravelAuthorizationDocument taDoc = null;
1241         taDoc = findCurrentTravelAuthorization(document);
1242         if(taDoc != null) {
1243             List<AccountingLine> lines = taDoc.getSourceAccountingLines();
1244             for(AccountingLine line: lines) {
1245                 taTotal = taTotal.add(line.getAmount());
1246             }
1247         }
1248         return taTotal;
1249 
1250     }
1251 
1252     /**
1253      * Determines if the user is a fiscal officer on {@link Account} instances tied to the {@link TravelAuthorizationDocument}
1254      * instance
1255      *
1256      * @param authorization to check for fiscal officer status on
1257      * @param principalId is a Person that might be a fiscal officer on account
1258      * @return if the <code>user</code> is a fiscal officer on accounts tied to the {@link TravelAuthorizationDocument}
1259      */
1260     @Override
1261     public boolean isResponsibleForAccountsOn(final TravelDocument document, String principalId) {
1262         final List<String> accounts = findAccountsResponsibleFor(document.getSourceAccountingLines(), principalId);
1263         return (accounts != null && accounts.size() > 0);
1264     }
1265 
1266     /**
1267      * Looks up accounts from {@link List} of {@link SourceAccountingLine} instances to determine if {@link Person} <code>user</code>
1268      * is a fiscal officer on any of those
1269      *
1270      * @param lines or {@link List} of {@link SourceAccountingLine} instances
1271      * @param principalId is a Person that might be a fiscal officer on accounts in <code>lines</code>
1272      * @return a {@link List} of account numbers the {@link Person} is a fiscal officer on
1273      */
1274     protected List<String> findAccountsResponsibleFor(final List<SourceAccountingLine> lines, String principalId) {
1275         final Set<String> accountList = new HashSet<String>();
1276         for (AccountingLine line : lines) {
1277             line.refreshReferenceObject(KFSPropertyConstants.ACCOUNT);
1278             if (line != null && !ObjectUtils.isNull(line.getAccount())) {
1279                 Person accountFiscalOfficerUser = line.getAccount().getAccountFiscalOfficerUser();
1280                 if (accountFiscalOfficerUser != null && accountFiscalOfficerUser.getPrincipalId().equals(principalId)) {
1281                     accountList.add(line.getAccountNumber());
1282                 }
1283             }
1284         }
1285         return new ArrayList<String>(accountList);
1286     }
1287 
1288     /**
1289      * This method checks to see if the type code is for a non-employee
1290      *
1291      * @param travelerTypeCode
1292      */
1293     @Override
1294     public boolean checkNonEmployeeTravelerTypeCode(String travelerTypeCode) {
1295         boolean foundCode = false;
1296         if (getParameterService().getParameterValuesAsString(TemParameterConstants.TEM_DOCUMENT.class, TravelParameters.NON_EMPLOYEE_TRAVELER_TYPE_CODES).contains(travelerTypeCode)) {
1297             foundCode = true;
1298         }
1299         return foundCode;
1300     }
1301 
1302 
1303     /**
1304      *
1305      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getAllStates(java.lang.String)
1306      */
1307     @Override
1308     public String getAllStates(final String countryCode) {
1309 
1310         final List<State> codes = getStateService().findAllStatesInCountry(countryCode);
1311 
1312         final StringBuffer sb = new StringBuffer();
1313         sb.append(";");
1314         for (final State state : codes) {
1315             if (state.isActive()) {
1316                 sb.append(state.getCode()).append(";");
1317             }
1318         }
1319 
1320         return sb.toString();
1321     }
1322 
1323     /**
1324      *
1325      * This method imports the file and convert it to a list of objects (of the class specified in the parameter)
1326      *
1327      * 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).
1328      *
1329      * @param formFile
1330      * @param c
1331      * @param attributeNames
1332      * @param tabErrorKey
1333      * @return
1334      */
1335     @Override
1336     public <T> List<T> importFile(final String fileContents, final Class<T> c, final String[] attributeNames, final Map<String,List<String>> defaultValues,
1337             final Integer[] attributeMaxLength, final String tabErrorKey) {
1338         if(attributeMaxLength != null && attributeNames.length != attributeMaxLength.length){
1339             throw new UploadParserException("Invalid parser configuration, the number of attribute names and attribute max length should be the same");
1340         }
1341 
1342         return importFile(fileContents, c, attributeNames, defaultValues, attributeMaxLength, tabErrorKey, getDefaultAcceptableFileExtensions());
1343     }
1344 
1345     /**
1346      *
1347      * This method imports the file and convert it to a list of objects (of the class specified in the parameter)
1348      * @param formFile
1349      * @param c
1350      * @param attributeNames
1351      * @param tabErrorKey
1352      * @param fileExtensions
1353      * @return
1354      */
1355     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) {
1356         final List<T> importedObjects = new ArrayList<T>();
1357 
1358         // parse file line by line
1359         Integer lineNo = 0;
1360         boolean failed = false;
1361         for (final String line : fileContents.split("\n")) {
1362             lineNo++;
1363             try {
1364                 final T o = parseLine(line, c, attributeNames, defaultValues, attributeMaxLength, lineNo, tabErrorKey);
1365                 importedObjects.add(o);
1366             }
1367             catch (UploadParserException e) {
1368                 // continue to parse the rest of the lines after the current line fails
1369                 // error messages are already dealt with inside parseFile, so no need to do anything here
1370                 failed = true;
1371             }
1372         }
1373 
1374         if (failed) {
1375             throw new UploadParserException("errors in parsing lines in file ", ERROR_UPLOADPARSER_LINE);
1376         }
1377 
1378         return importedObjects;
1379     }
1380 
1381     /**
1382      *
1383      * This method parses a CSV line
1384      * @param line
1385      * @param c
1386      * @param attributeNames
1387      * @param lineNo
1388      * @param tabErrorKey
1389      * @return
1390      */
1391     protected <T> T parseLine(String line, Class<T> c, String[] attributeNames,Map<String, List<String>> defaultValues, Integer[] attributeMaxLength, Integer lineNo, String tabErrorKey) {
1392         final Map<String, String> objectMap = retrieveObjectAttributes(line, attributeNames, defaultValues, attributeMaxLength, lineNo, tabErrorKey);
1393         final T obj = genObjectWithRetrievedAttributes(objectMap, c, lineNo, tabErrorKey);
1394         ((PersistableBusinessObject) obj).refresh();
1395         return obj;
1396     }
1397 
1398     /**
1399      *
1400      * This method generates an object instance and populates it with the specified attribute map.
1401      * @param objectMap
1402      * @param c
1403      * @param lineNo
1404      * @param tabErrorKey
1405      * @return
1406      */
1407     protected <T> T genObjectWithRetrievedAttributes(final Map<String, String> objectMap,
1408             final Class<T> c, final Integer lineNo, final String tabErrorKey) {
1409         T object;
1410         try {
1411             object = c.newInstance();
1412         }
1413         catch (Exception e) {
1414             throw new InfrastructureException("unable to complete line population.", e);
1415         }
1416 
1417         boolean failed = false;
1418         for (final Map.Entry<String, String> entry : objectMap.entrySet()) {
1419             try {
1420                 try {
1421                     ObjectUtils.setObjectProperty(object, entry.getKey(), entry.getValue());
1422                 }
1423                 catch (FormatException e) {
1424                     String[] errorParams = { entry.getValue(), entry.getKey(), "" + lineNo };
1425                     throw new UploadParserException("invalid numeric property value: "
1426                             + entry.getKey() + " = " + entry.getValue() + " (line " + lineNo + ")", ERROR_UPLOADPARSER_INVALID_NUMERIC_VALUE, errorParams);
1427                 }
1428             }
1429             catch (UploadParserException e) {
1430                 // continue to parse the rest of the properties after the current property fails
1431                 GlobalVariables.getMessageMap().putError(tabErrorKey, e.getErrorKey(), e.getErrorParameters());
1432                 failed = true;
1433             }
1434             catch (NoSuchMethodException nsme) {
1435                 throw new RuntimeException("Could not set property while parsing group travelers csv", nsme);
1436             }
1437             catch (InvocationTargetException ite) {
1438                 throw new RuntimeException("Could not set property while parsing group travelers csv", ite);
1439             }
1440             catch (IllegalAccessException iae) {
1441                 throw new RuntimeException("Could not set property while parsing group travelers csv", iae);
1442             }
1443         }
1444 
1445         if (failed) {
1446             throw new UploadParserException("empty or invalid properties in line " + lineNo + ")", ERROR_UPLOADPARSER_PROPERTY, ""+lineNo);
1447         }
1448         return object;
1449     }
1450 
1451     /**
1452      *
1453      * This method retrieves the attributes as key-value string pairs into a map.
1454      * @param line
1455      * @param attributeNames
1456      * @param lineNo
1457      * @param tabErrorKey
1458      * @return
1459      */
1460     protected Map<String, String> retrieveObjectAttributes(String line,
1461             String[] attributeNames,
1462             Map<String, List<String>> defaultValues,
1463             Integer[] attributeMaxLength,
1464             Integer lineNo, String tabErrorKey) {
1465         String[] attributeValues = StringUtils.splitPreserveAllTokens(line, ',');
1466         if (attributeNames.length != attributeValues.length) {
1467             String[] errorParams = { "" + attributeNames.length, "" + attributeValues.length, "" + lineNo };
1468             GlobalVariables.getMessageMap().putError(tabErrorKey, ERROR_UPLOADPARSER_WRONG_PROPERTY_NUMBER, errorParams);
1469             throw new UploadParserException("wrong number of properties: " + attributeValues.length + " exist, " + attributeNames.length + " expected (line " + lineNo + ")", ERROR_UPLOADPARSER_WRONG_PROPERTY_NUMBER, errorParams);
1470         }
1471 
1472         for (int i = 0; i < attributeNames.length; i++) {
1473             if (defaultValues != null && defaultValues.get(attributeNames[i]) != null) {
1474                 List<String> defaultValue = defaultValues.get(attributeNames[i]);
1475                 boolean found = false;
1476                 for (String value : defaultValue) {
1477                     if (attributeValues[i].equalsIgnoreCase(value)) {
1478                         found = true;
1479                     }
1480                 }
1481                 if (!found) {
1482                     GlobalVariables.getMessageMap().putWarning(tabErrorKey, MESSAGE_UPLOADPARSER_INVALID_VALUE, attributeNames[i], attributeValues[i], (" " + lineNo));
1483                     throw new UploadParserException("Invalid value " + attributeValues[i] + " exist, " + "in line (" + lineNo + ")", ERROR_UPLOADPARSER_WRONG_PROPERTY_NUMBER);
1484                 }
1485             }
1486 
1487             if (attributeMaxLength != null) {
1488                 if (attributeValues[i] != null && attributeValues[i].length() > attributeMaxLength[i]) {
1489                     attributeValues[i] = attributeValues[i].substring(0, attributeMaxLength[i]);
1490                     String[] errorParams = { "" + attributeNames[i], "" + attributeMaxLength[i], "" + lineNo };
1491                     GlobalVariables.getMessageMap().putWarning(tabErrorKey, MESSAGE_UPLOADPARSER_EXCEEDED_MAX_LENGTH, errorParams);
1492                 }
1493             }
1494         }
1495 
1496         Map<String, String> objectMap = new HashMap<String, String>();
1497         for (int i = 0; i < attributeNames.length; i++) {
1498             objectMap.put(attributeNames[i], attributeValues[i]);
1499         }
1500 
1501         return objectMap;
1502     }
1503 
1504     /**
1505      * Parses a header into some usable form that can be used to parse records from the
1506      * CSV
1507      *
1508      * @param csvHeader is an array of columns for a csv record
1509      * @return a {@link Map} keyed by field names to their column numbers
1510      */
1511     protected Map<String, List<Integer>> parseHeader(final String[] csvHeader) {
1512         final Map<String, List<Integer>> retval = new HashMap<String, List<Integer>>();
1513 
1514         for (Integer i = 0; i < csvHeader.length; i++) {
1515 
1516             if (StringUtils.isBlank(csvHeader[i].trim())) {
1517                 final String formattedName = nextHeader(csvHeader, i);
1518                 final Integer start = i;
1519                 final Integer end = csvHeader.length > i ? nextBlankHeader(csvHeader, i) : i;
1520 
1521                 final List<Integer> indexes = new ArrayList<Integer>();
1522 
1523                 for (Integer y = start; y < end; y++) {
1524                     indexes.add(y);
1525                 }
1526                 retval.put(formattedName, indexes);
1527             }
1528             else {
1529                 final String formattedName = toCamelCase(csvHeader[i]);
1530 
1531                 if (StringUtils.isNotBlank(formattedName)) {
1532                     retval.put(formattedName, Arrays.asList(new Integer[] { i }));
1533                 }
1534             }
1535         }
1536         return retval;
1537     }
1538 
1539     protected String nextHeader(final String[] headers, final int start) {
1540         for (int i = start + 1; i < headers.length; i++) {
1541             if (StringUtils.isNotBlank(headers[i])) {
1542                 return toCamelCase(headers[i]);
1543             }
1544         }
1545         return "";
1546     }
1547 
1548 
1549     protected Integer nextBlankHeader(final String[] headers, final int start) {
1550         for (int i = start + 1; i < headers.length; i++) {
1551             if (StringUtils.isBlank(headers[i])) {
1552                 return i;
1553             }
1554         }
1555         return -1;
1556     }
1557 
1558     protected String toProperCase(final String s) {
1559         if (s == null || s.length() < 1) {
1560             return "";
1561         }
1562 
1563         final char[] arr = s.toLowerCase().toCharArray();
1564         arr[0] = Character.toUpperCase(arr[0]);
1565 
1566         return new String(arr);
1567     }
1568 
1569     protected String toCamelCase(final String s) {
1570         final StringBuffer buffer = new StringBuffer();
1571 
1572         final List<String> words = new LinkedList<String>(Arrays.asList(s.toLowerCase().trim().replace('_', ' ').split(" ")));
1573         buffer.append(words.remove(0));
1574 
1575         for (final String word : words) {
1576             buffer.append(toProperCase(word));
1577         }
1578         return buffer.toString();
1579     }
1580     /**
1581      *
1582      */
1583     @Override
1584     public List<GroupTraveler> importGroupTravelers(final TravelDocument document, final String csvData) throws Exception {
1585         final List<GroupTraveler> retval = new LinkedList<GroupTraveler>();
1586         final BufferedReader bufferedFileReader = new BufferedReader(new StringReader(csvData));
1587         final CSVReader csvReader = new CSVReader(bufferedFileReader);
1588 
1589         final List<String[]> rows;
1590         try {
1591             rows = csvReader.readAll();
1592         }
1593         catch (IOException ex) {
1594             ex.printStackTrace();
1595             throw new ParseException("Could not  parse CSV file data", ex);
1596         }
1597         finally {
1598             try {
1599                 csvReader.close();
1600             }
1601             catch (Exception e) {}
1602         }
1603 
1604         final Map<String,List<Integer>> header = getGroupTravelerHeaders();
1605 
1606         for (final String[] row : rows) {
1607             final GroupTravelerCsvRecord record = createGroupTravelerCsvRecord(header, row);
1608             final GroupTraveler traveler = new GroupTraveler();
1609             traveler.setGroupTravelerEmpId(record.getGroupTravelerEmpId());
1610             traveler.setName(record.getName());
1611             traveler.setGroupTravelerType(record.getGroupTravelerType());
1612             retval.add(traveler);
1613         }
1614 
1615         return retval;
1616     }
1617 
1618     protected GroupTravelerCsvRecord createGroupTravelerCsvRecord(final Map<String, List<Integer>> header, final String[] record) throws Exception {
1619         return getCsvRecordFactory().newInstance(header, record);
1620     }
1621 
1622     @Override
1623     public boolean isUnsuccessful(TravelDocument document) {
1624         String status = document.getDocumentHeader().getWorkflowDocument().getStatus().getCode();
1625         List<String> unsuccessful = KewApiConstants.DOCUMENT_STATUS_PARENT_TYPES.get(KewApiConstants.DOCUMENT_STATUS_PARENT_TYPE_UNSUCCESSFUL);
1626         for (String tempStatus : unsuccessful){
1627             if (status.equals(tempStatus)){
1628                 return true;
1629             }
1630         }
1631 
1632         return false;
1633     }
1634 
1635     /**
1636      * 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)
1637      * @return a Map of columns and positions
1638      */
1639     protected Map<String,List<Integer>> getGroupTravelerHeaders() {
1640         Map<String, List<Integer>> headers = new HashMap<String, List<Integer>>();
1641         if (getGroupTravelerColumns() != null && !getGroupTravelerColumns().isEmpty()) {
1642             int count = 0;
1643             while (count < getGroupTravelerColumns().size()) {
1644                 List<Integer> countArray = new ArrayList<Integer>(2);
1645                 countArray.add(new Integer(count));
1646                 final String columnName = getGroupTravelerColumns().get(count);
1647                 headers.put(columnName, countArray);
1648                 count += 1;
1649             }
1650         }
1651         return headers;
1652     }
1653 
1654     /**
1655      *
1656      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copyGroupTravelers(java.util.List, java.lang.String)
1657      */
1658     @Override
1659     public List<GroupTraveler> copyGroupTravelers(List<GroupTraveler> groupTravelers, String documentNumber) {
1660         List<GroupTraveler> newGroupTravelers = new ArrayList<GroupTraveler>();
1661         if (groupTravelers != null) {
1662             for (GroupTraveler groupTraveler : groupTravelers) {
1663                 GroupTraveler newGroupTraveler = new GroupTraveler();
1664                 BeanUtils.copyProperties(groupTraveler, newGroupTraveler);
1665                 newGroupTraveler.setDocumentNumber(documentNumber);
1666                 newGroupTraveler.setVersionNumber(new Long(1));
1667                 newGroupTraveler.setObjectId(null);
1668                 newGroupTraveler.setId(null);
1669                 newGroupTravelers.add(newGroupTraveler);
1670             }
1671         }
1672         return newGroupTravelers;
1673     }
1674 
1675     /**
1676      *
1677      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copyActualExpenses(java.util.List, java.lang.String)
1678      */
1679     @Override
1680     public List<? extends TemExpense> copyActualExpenses(List<? extends TemExpense> actualExpenses, String documentNumber) {
1681         List<ActualExpense> newActualExpenses = new ArrayList<ActualExpense>();
1682 
1683         if (actualExpenses != null) {
1684             for (TemExpense expense : actualExpenses) {
1685                 ActualExpense actualExpense = (ActualExpense)expense;
1686                 ActualExpense newActualExpense = new ActualExpense();
1687                 boolean nullCheck = false;
1688                 if (actualExpense.getExpenseDate() == null) {
1689                     nullCheck = true;
1690                     actualExpense.setExpenseDate(new Date(0));
1691                 }
1692                 BeanUtils.copyProperties(actualExpense, newActualExpense);
1693                 if (nullCheck) {
1694                     actualExpense.setExpenseDate(null);
1695                     newActualExpense.setExpenseDate(null);
1696                 }
1697 
1698                 List<TemExpense> newDetails = (List<TemExpense>) this.copyActualExpenses(actualExpense.getExpenseDetails(), documentNumber);
1699                 newActualExpense.setExpenseDetails(newDetails);
1700                 newActualExpense.setDocumentNumber(documentNumber);
1701                 newActualExpense.setVersionNumber(new Long(1));
1702                 newActualExpense.setId(null);
1703                 newActualExpense.setObjectId(null);
1704                 newActualExpenses.add(newActualExpense);
1705             }
1706         }
1707         return newActualExpenses;
1708     }
1709 
1710     private Long getNextActualExpenseId(){
1711         return SpringContext.getBean(SequenceAccessorService.class).getNextAvailableSequenceNumber(TemConstants.TEM_ACTUAL_EXPENSE_SEQ_NAME);
1712     }
1713 
1714     /**
1715      *
1716      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copyPerDiemExpenses(java.util.List, java.lang.String)
1717      */
1718     @Override
1719     public List<PerDiemExpense> copyPerDiemExpenses(List<PerDiemExpense> perDiemExpenses, String documentNumber) {
1720         List<PerDiemExpense> newPerDiemExpenses = new ArrayList<PerDiemExpense>();
1721         if (perDiemExpenses != null) {
1722             for (PerDiemExpense expense : perDiemExpenses){
1723                 PerDiemExpense newExpense = new PerDiemExpense();
1724                 BeanUtils.copyProperties(expense, newExpense);
1725                 newExpense.setBreakfastValue(expense.getBreakfastValue());
1726                 newExpense.setLunchValue(expense.getLunchValue());
1727                 newExpense.setDinnerValue(expense.getDinnerValue());
1728                 newExpense.setIncidentalsValue(expense.getIncidentalsValue());
1729                 newExpense.setDocumentNumber(documentNumber);
1730                 newExpense.setVersionNumber(new Long(1));
1731                 newExpense.setObjectId(null);
1732                 newExpense.setId(null);
1733                 newPerDiemExpenses.add(newExpense);
1734             }
1735         }
1736         return newPerDiemExpenses;
1737     }
1738 
1739     /**
1740      *
1741      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copyTravelAdvances(java.util.List, java.lang.String)
1742      */
1743     @Override
1744     public List<TravelAdvance> copyTravelAdvances(List<TravelAdvance> travelAdvances, String documentNumber) {
1745         List<TravelAdvance> newTravelAdvances = new ArrayList<TravelAdvance>();
1746         if (travelAdvances != null) {
1747             for (TravelAdvance travelAdvance : travelAdvances){
1748                 TravelAdvance newTravelAdvance = (TravelAdvance) ObjectUtils.deepCopy(travelAdvance);
1749                 newTravelAdvance.setDocumentNumber(documentNumber);
1750                 newTravelAdvance.setVersionNumber(new Long(1));
1751                 newTravelAdvance.setObjectId(null);
1752                 newTravelAdvance.setTravelDocumentIdentifier(travelAdvance.getTravelDocumentIdentifier());
1753                 newTravelAdvances.add(newTravelAdvance);
1754             }
1755         }
1756         return newTravelAdvances;
1757     }
1758 
1759     /**
1760      *
1761      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copySpecialCircumstances(java.util.List, java.lang.String)
1762      */
1763     @Override
1764     public List<SpecialCircumstances> copySpecialCircumstances(List<SpecialCircumstances> specialCircumstancesList, String documentNumber) {
1765         List<SpecialCircumstances> newSpecialCircumstancesList = new ArrayList<SpecialCircumstances>();
1766         if (specialCircumstancesList != null) {
1767             for (SpecialCircumstances specialCircumstances : specialCircumstancesList){
1768                 SpecialCircumstances newSpecialCircumstances = new SpecialCircumstances();
1769                 BeanUtils.copyProperties(specialCircumstances, newSpecialCircumstances);
1770                 newSpecialCircumstances.setDocumentNumber(documentNumber);
1771                 newSpecialCircumstances.setVersionNumber(new Long(1));
1772                 newSpecialCircumstances.setObjectId(null);
1773                 newSpecialCircumstances.setId(null);
1774                 newSpecialCircumstancesList.add(newSpecialCircumstances);
1775             }
1776         }
1777         return newSpecialCircumstancesList;
1778     }
1779 
1780     /**
1781      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#copyTransportationModeDetails(java.util.List, java.lang.String)
1782      */
1783     @Override
1784     public List<TransportationModeDetail> copyTransportationModeDetails(List<TransportationModeDetail> transportationModeDetails, String documentNumber) {
1785         List<TransportationModeDetail> newTransportationModeDetails = new ArrayList<TransportationModeDetail>();
1786         if (transportationModeDetails != null) {
1787             for (TransportationModeDetail detail : transportationModeDetails){
1788                 TransportationModeDetail newDetail = new TransportationModeDetail();
1789                 BeanUtils.copyProperties(detail, newDetail);
1790                 newDetail.setDocumentNumber(documentNumber);
1791                 newDetail.setVersionNumber(new Long(1));
1792                 newDetail.setObjectId(null);
1793                 newTransportationModeDetails.add(newDetail);
1794             }
1795         }
1796         return newTransportationModeDetails;
1797     }
1798 
1799     /**
1800      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#showNoTravelAuthorizationError(org.kuali.kfs.module.tem.document.TravelReimbursementDocument)
1801      */
1802     @Override
1803     public void showNoTravelAuthorizationError(TravelReimbursementDocument document){
1804         if (document.getTripType() != null && document.getTripType().getTravelAuthorizationRequired()){
1805             TravelAuthorizationDocument authorization = findCurrentTravelAuthorization(document);
1806             if (authorization == null){
1807                 GlobalVariables.getMessageMap().putError(KRADPropertyConstants.DOCUMENT + "." + TemPropertyConstants.TRIP_TYPE_CODE, TemKeyConstants.ERROR_TRIP_TYPE_TA_REQUIRED, document.getTripType().getName());
1808             }
1809         }
1810     }
1811 
1812     /**
1813      * @see org.kuali.kfs.module.tem.document.service.TravelAuthorizationService#getAdvancesTotalFor(TravelDocument)
1814      */
1815     @Override
1816     public KualiDecimal getAdvancesTotalFor(TravelDocument travelDocument) {
1817         KualiDecimal retval = KualiDecimal.ZERO;
1818         if (ObjectUtils.isNull(travelDocument)) {
1819             return retval;
1820         }
1821 
1822         LOG.debug("Looking for travel advances for travel: "+ travelDocument.getDocumentNumber());
1823 
1824         TravelAuthorizationDocument authorization = null;
1825         authorization = findCurrentTravelAuthorization(travelDocument);
1826 
1827         if (authorization == null) {
1828             return retval;
1829         }
1830         authorization.refreshReferenceObject(TemPropertyConstants.TRVL_ADV);
1831 
1832         if (authorization.shouldProcessAdvanceForDocument()) {
1833             retval = retval.add(authorization.getTravelAdvance().getTravelAdvanceRequested());
1834         }
1835         return retval;
1836     }
1837 
1838     /**
1839      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#retrieveAddressFromLocationCode(java.lang.String)
1840      */
1841     @Override
1842     public String retrieveAddressFromLocationCode(String locationCode) {
1843         PaymentDocumentationLocation dvDocumentLocation = businessObjectService.findBySinglePrimaryKey(PaymentDocumentationLocation.class, locationCode);
1844         String address = ObjectUtils.isNotNull(dvDocumentLocation)? dvDocumentLocation.getPaymentDocumentationLocationAddress() : "";
1845         return address;
1846     }
1847 
1848     @Override
1849     public boolean validateSourceAccountingLines(TravelDocument travelDocument, boolean addToErrorPath) {
1850         boolean success = true;
1851         Map<String,Object> fieldValues = new HashMap<String,Object>();
1852         fieldValues.put(KRADPropertyConstants.DOCUMENT_NUMBER, travelDocument.getDocumentNumber());
1853         fieldValues.put(KFSPropertyConstants.FINANCIAL_DOCUMENT_LINE_TYPE_CODE, KFSConstants.SOURCE_ACCT_LINE_TYPE_CODE);
1854 
1855         List<TemSourceAccountingLine> currentLines = (List<TemSourceAccountingLine>) getBusinessObjectService().findMatchingOrderBy(TemSourceAccountingLine.class, fieldValues,KFSPropertyConstants.SEQUENCE_NUMBER, true);
1856 
1857         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
1858                                         // was a permission granted to the travel manager so we should allow it
1859 
1860         for (int i=0;i<travelDocument.getSourceAccountingLines().size();i++){
1861             AccountingLine line = (AccountingLine) travelDocument.getSourceAccountingLines().get(i);
1862             if (addToErrorPath){
1863                 GlobalVariables.getMessageMap().getErrorPath().add("document." + TemPropertyConstants.SOURCE_ACCOUNTING_LINE + "[" + i + "]");
1864             }
1865             if(StringUtils.isBlank(line.getAccountNumber())){
1866                 success = false;
1867                 GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_NUMBER, KFSKeyConstants.ERROR_REQUIRED, "Account Number");
1868             }
1869             else{
1870                 if ((!travelDocument.getAppDocStatus().equalsIgnoreCase("Initiated"))
1871                         && (!travelDocument.getAppDocStatus().equalsIgnoreCase(TemConstants.TravelAuthorizationStatusCodeKeys.IN_PROCESS))
1872                         && (!travelDocument.getAppDocStatus().equalsIgnoreCase(TemConstants.TravelAuthorizationStatusCodeKeys.CHANGE_IN_PROCESS))){
1873                     if ((i < currentLines.size()) && (!(currentLines.get(i)).getAccountNumber().equals(line.getAccountNumber()))
1874                             || (i >= currentLines.size())){
1875                         try{
1876                             if (!line.getAccount().getAccountFiscalOfficerUser().getPrincipalId().equals(GlobalVariables.getUserSession().getPerson().getPrincipalId())
1877                                     && !canUpdate){
1878                                 GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_NUMBER, TemKeyConstants.ERROR_TA_FISCAL_OFFICER_ACCOUNT, line.getAccountNumber());
1879                                 success = false;
1880                             }
1881                         }
1882                         catch(Exception e){
1883                             //do nothing, other validation will figure out this account doesn't exist
1884                         }
1885                     }
1886                 }
1887             }
1888             if(StringUtils.isBlank(line.getChartOfAccountsCode())){
1889                 success = false;
1890                 GlobalVariables.getMessageMap().putError(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, KFSKeyConstants.ERROR_REQUIRED, "Chart");
1891             }
1892             if (addToErrorPath){
1893                 GlobalVariables.getMessageMap().getErrorPath().remove(GlobalVariables.getMessageMap().getErrorPath().size()-1);
1894             }
1895         }
1896 
1897         return success;
1898     }
1899 
1900     /**
1901      * This method parses out the options from the parameters table and sets boolean values for each one LODGING MILEAGE PER_DIEM
1902      *
1903      * @param perDiemCats
1904      */
1905     private boolean showPerDiem(List<String> perDiemCats, String perDiemType) {
1906         for (String category : perDiemCats) {
1907             String[] pair = category.split("=");
1908             if (pair[0].equalsIgnoreCase(perDiemType)) {
1909                 return pair[1].equalsIgnoreCase(TemConstants.YES);
1910             }
1911             if (pair[0].equalsIgnoreCase(perDiemType)) {
1912                 return pair[1].equalsIgnoreCase(TemConstants.YES);
1913             }
1914             if (pair[0].equalsIgnoreCase(perDiemType)) {
1915                 return pair[1].equalsIgnoreCase(TemConstants.YES);
1916             }
1917         }
1918 
1919         return false;
1920     }
1921 
1922     /**
1923      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getOutstandingTravelAdvanceByInvoice(java.util.Set)
1924      */
1925     @Override
1926     public List<TravelAdvance> getOutstandingTravelAdvanceByInvoice(Set<String> arInvoiceDocNumbers) {
1927         return travelDocumentDao.getOutstandingTravelAdvanceByInvoice(arInvoiceDocNumbers);
1928     }
1929 
1930     /**
1931      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#findLatestTaxableRamificationNotificationDate()
1932      */
1933     @Override
1934     public Date findLatestTaxableRamificationNotificationDate() {
1935         Object[] returnResult =  travelDocumentDao.findLatestTaxableRamificationNotificationDate();
1936         Date date = null;
1937         try {
1938           date =  ObjectUtils.isNotNull(returnResult[0])? dateTimeService.convertToSqlDate((Timestamp)returnResult[0]): null;
1939         }catch (java.text.ParseException ex) {
1940             LOG.error("Invalid latest taxable ramification notification date " + returnResult[0]);
1941         }
1942 
1943         return date;
1944     }
1945 
1946     @Override
1947     public void detachImportedExpenses(TravelDocument document) {
1948         for (ImportedExpense importedExpense : document.getImportedExpenses()){
1949             ExpenseUtils.assignExpense(importedExpense.getHistoricalTravelExpenseId(), null, null, null, false);
1950         }
1951         document.setImportedExpenses(new ArrayList<ImportedExpense>());
1952         document.setHistoricalTravelExpenses(new ArrayList<HistoricalTravelExpense>());
1953     }
1954 
1955     @Override
1956     public void attachImportedExpenses(TravelDocument document) {
1957         for (ImportedExpense importedExpense : document.getImportedExpenses()){
1958             ExpenseUtils.assignExpense(importedExpense.getHistoricalTravelExpenseId(), document.getTravelDocumentIdentifier(),document.getDocumentNumber(), document.getFinancialDocumentTypeCode(), true);
1959         }
1960     }
1961 
1962     /**
1963      * Check to see if the hold new fiscal year encumbrance indicator is true
1964      * and the trip end date is after the current fiscal year end date to determine
1965      * whether or not to mark all the GLPEs as 'H' (Hold)
1966      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#holdGLPEs(org.kuali.kfs.module.tem.document.TravelDocument)
1967      */
1968     @Override
1969     public boolean checkHoldGLPEs(TravelDocument document) {
1970         if(getParameterService().getParameterValueAsBoolean(TravelAuthorizationDocument.class, TemConstants.TravelAuthorizationParameters.HOLD_NEW_FISCAL_YEAR_ENCUMBRANCES_IND)) {
1971 
1972             java.util.Date endDate = getUniversityDateService().getLastDateOfFiscalYear(getUniversityDateService().getCurrentFiscalYear());
1973             if (ObjectUtils.isNotNull(document.getTripBegin()) && document.getTripBegin().after(endDate)) {
1974                 return true;
1975             }
1976 
1977         }
1978 
1979         return false;
1980     }
1981 
1982     /**
1983      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#revertOriginalDocument(org.kuali.kfs.module.tem.document.TravelDocument, java.lang.String)
1984      */
1985     @Override
1986     public void revertOriginalDocument(TravelDocument travelDocument, String status) {
1987         final DocumentAttributeIndexingQueue documentAttributeIndexingQueue = KewApiServiceLocator.getDocumentAttributeIndexingQueue(); // this service is not a good candidate for injection
1988         List<Document> relatedDocumentList = getDocumentsRelatedTo(travelDocument, TravelDocTypes.TRAVEL_AUTHORIZATION_DOCUMENT,
1989                 TravelDocTypes.TRAVEL_AUTHORIZATION_AMEND_DOCUMENT);
1990 
1991         for (Document taDocument : relatedDocumentList) {
1992             if (taDocument.getDocumentHeader().getWorkflowDocument().getApplicationDocumentStatus().equals(TravelAuthorizationStatusCodeKeys.PEND_AMENDMENT)) {
1993                 TravelAuthorizationDocument taDoc = (TravelAuthorizationDocument) taDocument;
1994                 try {
1995                     taDoc.updateAndSaveAppDocStatus(status);
1996                 }
1997                 catch (WorkflowException ex1) {
1998                     // TODO Auto-generated catch block
1999                     ex1.printStackTrace();
2000                 }
2001 
2002                 try {
2003                     Note cancelNote = getDocumentService().createNoteFromDocument(taDoc, "Amemdment Canceled");
2004                     Principal systemUser = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(KFSConstants.SYSTEM_USER);
2005                     cancelNote.setAuthorUniversalIdentifier(systemUser.getPrincipalId());
2006                     taDoc.addNote(cancelNote);
2007                     getNoteService().save(cancelNote);
2008                 }
2009                 catch (Exception ex) {
2010                     ex.printStackTrace();
2011                 }
2012                 documentAttributeIndexingQueue.indexDocument(taDoc.getDocumentNumber());
2013             }
2014         }
2015     }
2016 
2017     /**
2018      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getDocumentType(org.kuali.kfs.module.tem.document.TravelDocument)
2019      */
2020     @Override
2021     public String getDocumentType(TravelDocument document) {
2022         String documentType = null;
2023 
2024         if (document != null) {
2025             if (document instanceof TravelAuthorizationDocument) {
2026                 documentType = TemConstants.TravelDocTypes.TRAVEL_AUTHORIZATION_DOCUMENT;
2027             }
2028             else if (document instanceof TravelReimbursementDocument) {
2029                 documentType = TemConstants.TravelDocTypes.TRAVEL_REIMBURSEMENT_DOCUMENT;
2030             }
2031             else if (document instanceof TravelEntertainmentDocument) {
2032                 documentType = TemConstants.TravelDocTypes.TRAVEL_ENTERTAINMENT_DOCUMENT;
2033             }
2034             else if (document instanceof TravelRelocationDocument) {
2035                 documentType = TemConstants.TravelDocTypes.TRAVEL_RELOCATION_DOCUMENT;
2036             }
2037         }
2038 
2039         return documentType;
2040     }
2041 
2042     /**
2043      * Check if workflow is at the specific node
2044      *
2045      * @param workflowDocument
2046      * @param nodeName
2047      * @return
2048      */
2049     protected boolean isAtTravelNode(WorkflowDocument workflowDocument) {
2050         Set<String> nodeNames = workflowDocument.getNodeNames();
2051         for (String nodeNamesNode : nodeNames) {
2052             if (TemWorkflowConstants.RouteNodeNames.AP_TRAVEL.equals(nodeNamesNode)) {
2053                 return true;
2054             }
2055         }
2056         return false;
2057     }
2058 
2059     /**
2060      * Returns all travel advances associated with the given trip id
2061      * @see org.kuali.kfs.module.tem.document.service.TravelReimbursementService#getTravelAdvancesForTrip(java.lang.String)
2062      */
2063     @Override
2064     public List<TravelAdvance> getTravelAdvancesForTrip(String travelDocumentIdentifier) {
2065         Map<String, String> criteria = new HashMap<String, String>();
2066         criteria.put(TemPropertyConstants.TRAVEL_DOCUMENT_IDENTIFIER, travelDocumentIdentifier);
2067         List<TravelAdvance> advances = new ArrayList<TravelAdvance>();
2068         final Collection<TravelAdvance> foundAdvances = getBusinessObjectService().findMatchingOrderBy(TravelAdvance.class, criteria, KFSPropertyConstants.DOCUMENT_NUMBER, true);
2069         for (TravelAdvance foundAdvance: foundAdvances) {
2070             if (foundAdvance.isAtLeastPartiallyFilledIn() && isDocumentApprovedOrExtracted(foundAdvance.getDocumentNumber())) {
2071                 advances.add(foundAdvance);
2072             }
2073         }
2074         return advances;
2075     }
2076 
2077 
2078 
2079     /**
2080      * Determines if the document with the given document number has been approved or not
2081      * @param documentNumber the document number of the document to check
2082      * @return true if the document has been approved, false otherwise
2083      */
2084     protected boolean isDocumentApprovedOrExtracted(String documentNumber) {
2085         final FinancialSystemDocumentHeader documentHeader = getBusinessObjectService().findBySinglePrimaryKey(FinancialSystemDocumentHeader.class, documentNumber);
2086         return KFSConstants.DocumentStatusCodes.APPROVED.equals(documentHeader.getFinancialDocumentStatusCode()) || KFSConstants.DocumentStatusCodes.Payments.EXTRACTED.equals(documentHeader.getFinancialDocumentStatusCode());
2087     }
2088 
2089     /**
2090      * Determines if the document with the given document number has been initiated or submitted for routing
2091      * @param documentNumber the document number of the document to check
2092      * @return true if the document has been approved, false otherwise
2093      */
2094     protected boolean isDocumentInitiatedOrEnroute(String documentNumber) {
2095         final FinancialSystemDocumentHeader documentHeader = getBusinessObjectService().findBySinglePrimaryKey(FinancialSystemDocumentHeader.class, documentNumber);
2096         return KFSConstants.DocumentStatusCodes.INITIATED.equals(documentHeader.getFinancialDocumentStatusCode()) || KFSConstants.DocumentStatusCodes.ENROUTE.equals(documentHeader.getFinancialDocumentStatusCode());
2097     }
2098 
2099     /**
2100      * Gets the {@link OrganizationOptions} to create a {@link AccountsReceivableDocumentHeader} for
2101      * {@link PaymentApplicationDocument}
2102      *
2103      * @return OrganizationOptions
2104      */
2105     @Override
2106     public AccountsReceivableOrganizationOptions getOrgOptions() {
2107         final String chartOfAccountsCode = parameterService.getParameterValueAsString(TravelAuthorizationDocument.class, TravelAuthorizationParameters.TRAVEL_ADVANCE_BILLING_CHART);
2108         final String organizationCode = parameterService.getParameterValueAsString(TravelAuthorizationDocument.class, TravelAuthorizationParameters.TRAVEL_ADVANCE_BILLING_ORGANIZATION);
2109 
2110         return getAccountsReceivableModuleService().getOrgOptionsIfExists(chartOfAccountsCode, organizationCode);
2111     }
2112 
2113     /**
2114      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#disableDuplicateExpenses(org.kuali.kfs.module.tem.document.TravelReimbursementDocument, org.kuali.kfs.module.tem.businessobject.ActualExpense)
2115      */
2116     @Override
2117     public void disableDuplicateExpenses(TravelDocument trDocument, ActualExpense actualExpense) {
2118         if (trDocument.getPerDiemExpenses() != null && !trDocument.getPerDiemExpenses().isEmpty()) {  // no per diems? then let's not bother
2119             if (actualExpense.getExpenseDetails() != null && !actualExpense.getExpenseDetails().isEmpty()) {
2120                 for (TemExpense detail : actualExpense.getExpenseDetails()) {
2121                     checkActualExpenseAgainstPerDiems(trDocument, (ActualExpense)detail);
2122                 }
2123             } else {
2124                 checkActualExpenseAgainstPerDiems(trDocument, actualExpense);
2125             }
2126         }
2127     }
2128 
2129     /**
2130      * Checks the given actual expense (or detail) against each of the per diems on the TR document to disable
2131      * @param trDocument the travel reimbursement with per diems to check against
2132      * @param actualExpense
2133      */
2134     protected void checkActualExpenseAgainstPerDiems(TravelDocument trDocument, ActualExpense actualExpense) {
2135         int i = 0;
2136         for (final PerDiemExpense perDiemExpense : trDocument.getPerDiemExpenses()) {
2137             List<DisabledPropertyMessage> messages = disableDuplicateExpenseForPerDiem(actualExpense, perDiemExpense, i);
2138             if (messages != null && !messages.isEmpty()) {
2139                 for (DisabledPropertyMessage message : messages) {
2140                     message.addToProperties(trDocument.getDisabledProperties());
2141                 }
2142             }
2143             i+=1;
2144         }
2145     }
2146 
2147     /**
2148      * 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
2149      * @param actualExpense the actual expense to check
2150      * @param perDiemExpense the per diem to check the actual expense against
2151      * @param otherExpenseLineCode the expense type code of the actual epxnese
2152      * @param perDiemCount the count of the per diems we have worked through
2153      * @return a List of any messages about disabled properties which occurred
2154      */
2155     protected List<DisabledPropertyMessage> disableDuplicateExpenseForPerDiem(ActualExpense actualExpense, PerDiemExpense perDiemExpense, int perDiemCount) {
2156         List<DisabledPropertyMessage> disabledPropertyMessages = new ArrayList<DisabledPropertyMessage>();
2157 
2158         if (actualExpense.getExpenseDate() == null){
2159             return disabledPropertyMessages;
2160         }
2161         final String expenseDate = getDateTimeService().toDateString(actualExpense.getExpenseDate());
2162         String meal = "";
2163         boolean valid = true;
2164 
2165         if (KfsDateUtils.isSameDay(perDiemExpense.getMileageDate(), actualExpense.getExpenseDate())) {
2166             if (perDiemExpense.getBreakfast() && actualExpense.isBreakfast() && (actualExpense.getExpenseType().isHosted() || actualExpense.getExpenseType().isGroupTravel())) {
2167                 meal = TemConstants.HostedMeals.HOSTED_BREAKFAST;
2168                 perDiemExpense.setBreakfast(false);
2169                 perDiemExpense.setBreakfastValue(KualiDecimal.ZERO);
2170                 valid = false;
2171             }
2172             else if (perDiemExpense.getLunch() && actualExpense.isLunch() && (actualExpense.getExpenseType().isHosted() || actualExpense.getExpenseType().isGroupTravel())) {
2173                 meal = TemConstants.HostedMeals.HOSTED_LUNCH;
2174                 perDiemExpense.setLunch(false);
2175                 perDiemExpense.setLunchValue(KualiDecimal.ZERO);
2176                 valid = false;
2177             }
2178             else if (perDiemExpense.getDinner() && actualExpense.isDinner() && (actualExpense.getExpenseType().isHosted() || actualExpense.getExpenseType().isGroupTravel())) {
2179                 meal = TemConstants.HostedMeals.HOSTED_DINNER;
2180                 perDiemExpense.setDinner(false);
2181                 perDiemExpense.setDinnerValue(KualiDecimal.ZERO);
2182                 valid = false;
2183             }
2184 
2185             if (!valid) {
2186                 String temp = String.format(PER_DIEM_EXPENSE_DISABLED, perDiemCount, meal);
2187                 String message = getMessageFrom(MESSAGE_TR_MEAL_ALREADY_CLAIMED, expenseDate, meal);
2188                 disabledPropertyMessages.add(new DisabledPropertyMessage(temp, message));
2189             }
2190 
2191             // KUALITEM-483 add in check for lodging
2192             if (perDiemExpense.getLodging().isGreaterThan(KualiDecimal.ZERO) && !StringUtils.isBlank(actualExpense.getExpenseTypeCode()) && TemConstants.ExpenseTypes.LODGING.equals(actualExpense.getExpenseTypeCode())) {
2193                 String temp = String.format(PER_DIEM_EXPENSE_DISABLED, perDiemCount, TemConstants.LODGING.toLowerCase());
2194                 String message = getMessageFrom(MESSAGE_TR_LODGING_ALREADY_CLAIMED, expenseDate);
2195                 perDiemExpense.setLodging(KualiDecimal.ZERO);
2196                 disabledPropertyMessages.add(new DisabledPropertyMessage(temp, message));
2197             }
2198         }
2199         return disabledPropertyMessages;
2200     }
2201 
2202 
2203     @Override
2204     public List<String> findMatchingTrips(TravelDocument travelDocument) {
2205 
2206         String travelDocumentIdentifier = travelDocument.getTravelDocumentIdentifier();
2207         Integer temProfileId = travelDocument.getTemProfileId();
2208         Timestamp earliestTripBeginDate = null;
2209         Timestamp greatestTripEndDate = null;
2210 
2211         List<TravelReimbursementDocument> documents = findReimbursementDocuments(travelDocumentIdentifier);
2212        for (TravelReimbursementDocument document : documents) {
2213            Timestamp tripBegin = document.getTripBegin();
2214            Timestamp tripEnd = document.getTripEnd();
2215            if (ObjectUtils.isNull(earliestTripBeginDate) && ObjectUtils.isNull(greatestTripEndDate)) {
2216                earliestTripBeginDate = tripBegin;
2217                greatestTripEndDate = tripEnd;
2218            }
2219            else {
2220                earliestTripBeginDate = tripBegin.before(earliestTripBeginDate) ? tripBegin :earliestTripBeginDate;
2221                greatestTripEndDate = tripEnd.after(greatestTripEndDate)? tripEnd : greatestTripEndDate;
2222 
2223                }
2224         }
2225 
2226        // TR with no TAs created from mainmenu
2227        if(documents.isEmpty() && ObjectUtils.isNotNull(travelDocument.getTripBegin()) && ObjectUtils.isNotNull(travelDocument.getTripEnd())) {
2228            earliestTripBeginDate = getTripBeginDate(travelDocument.getTripBegin());
2229            greatestTripEndDate = getTripEndDate(travelDocument.getTripEnd());
2230        }
2231 
2232        List<TravelReimbursementDocument> matchDocs =  (List<TravelReimbursementDocument>) travelDocumentDao.findMatchingTrips(temProfileId ,earliestTripBeginDate, greatestTripEndDate);
2233         List<String> documentIds = new ArrayList<String>();
2234         for (TravelReimbursementDocument document : matchDocs) {
2235             if(!travelDocument.getDocumentNumber().equals(document.getDocumentNumber())) {
2236                 documentIds.add(document.getDocumentNumber());
2237             }
2238         }
2239         return documentIds;
2240     }
2241 
2242     private Integer getDuplicateTripDateRangeDays() {
2243         String tripDateRangeDays = parameterService.getParameterValueAsString(TravelAuthorizationDocument.class, TemConstants.TravelParameters.DUPLICATE_TRIP_DATE_RANGE_DAYS);
2244         Integer days = null;
2245         if (!StringUtils.isNumeric(tripDateRangeDays)) {
2246             days = TemConstants.DEFAULT_DUPLICATE_TRIP_DATE_RANGE_DAYS;
2247        }
2248 
2249        days = Integer.parseInt(tripDateRangeDays);
2250        return days;
2251 
2252     }
2253 
2254     private Timestamp getTripBeginDate(Timestamp tripBeginDate) {
2255         Timestamp tripBegin = null;
2256         Integer days = getDuplicateTripDateRangeDays();
2257          try {
2258              tripBegin = dateTimeService.convertToSqlTimestamp(dateTimeService.toDateString(DateUtils.addDays(tripBeginDate, (days * -1))));
2259 
2260          } catch (java.text.ParseException pe) {
2261              LOG.error("Exception while parsing trip begin date" + pe);
2262          }
2263 
2264 
2265          return tripBegin;
2266 
2267      }
2268 
2269      private Timestamp getTripEndDate(Timestamp tripEndDate) {
2270          Timestamp tripEnd = null;
2271          Integer days = getDuplicateTripDateRangeDays();
2272           try {
2273               tripEnd = dateTimeService.convertToSqlTimestamp(dateTimeService.toDateString((DateUtils.addDays(tripEndDate, days ))));
2274 
2275           } catch (java.text.ParseException pe) {
2276               LOG.error("Exception while parsing trip end date" + pe);
2277           }
2278 
2279           return tripEnd;
2280 
2281      }
2282 
2283 
2284     /**
2285      * Inner class to hold keys & messages for disabled properties
2286      */
2287     class DisabledPropertyMessage {
2288         private String key;
2289         private String message;
2290 
2291         DisabledPropertyMessage(String key, String message) {
2292             this.key = key;
2293             this.message = message;
2294         }
2295 
2296         void addToProperties(Map<String, String> messageMap) {
2297             messageMap.put(key, message);
2298         }
2299     }
2300 
2301     /**
2302      *
2303      * This method gets the current travel document by travel document identifier
2304      * @param travelDocumentIdentifier
2305      * @return
2306      */
2307     @Override
2308     public TravelDocument getParentTravelDocument(String travelDocumentIdentifier) {
2309 
2310        if (ObjectUtils.isNull(travelDocumentIdentifier) || StringUtils.equals(travelDocumentIdentifier,"")) {
2311            LOG.error("Received a null tripId/travelDocumentIdentifier; returning a null TravelDocument");
2312            return null;
2313        }
2314 
2315        try {
2316            TravelDocument travelDocument = findRootForTravelReimbursement(travelDocumentIdentifier);
2317            if (ObjectUtils.isNotNull(travelDocument)) {
2318                LOG.debug("Found "+ travelDocument.getDocumentNumber() +" ("+ travelDocument.getDocumentTypeName() +") for travelDocumentIdentifier: "+ travelDocumentIdentifier);
2319                return travelDocument;
2320            }
2321 
2322        } catch (Exception exception) {
2323            LOG.error("Exception occurred attempting to retrieve an authorization or remibursement travel document for travelDocumentIdentifier: "+ travelDocumentIdentifier, exception);
2324            return null;
2325        }
2326 
2327        Map<String, Object> fieldValues = new HashMap<String, Object>();
2328        fieldValues.put(TemPropertyConstants.TRAVEL_DOCUMENT_IDENTIFIER, travelDocumentIdentifier);
2329        fieldValues.put(TemPropertyConstants.TRIP_PROGENITOR, Boolean.TRUE);
2330 
2331        Collection<TravelEntertainmentDocument> entDocuments = getBusinessObjectService().findMatching(TravelEntertainmentDocument.class, fieldValues);
2332        if (entDocuments.iterator().hasNext()) {
2333            TravelDocument ent = entDocuments.iterator().next();
2334            LOG.debug("Found "+ ent.getDocumentNumber() +" ("+ ent.getDocumentTypeName() +") for travelDocumentIdentifier: "+ travelDocumentIdentifier);
2335            return ent;
2336        }
2337 
2338        Collection<TravelRelocationDocument> reloDocuments = getBusinessObjectService().findMatching(TravelRelocationDocument.class, fieldValues);
2339        if (reloDocuments.iterator().hasNext()) {
2340            TravelDocument relo = reloDocuments.iterator().next();
2341            LOG.info("Found "+ relo.getDocumentNumber() +" ("+ relo.getDocumentTypeName() +") for travelDocumentIdentifier: "+ travelDocumentIdentifier);
2342            return relo;
2343        }
2344 
2345        LOG.error("Unable to find any travel document for given Trip Id: "+ travelDocumentIdentifier);
2346        return null;
2347     }
2348 
2349     /**
2350      * Calculate the total of the source accounting lines on the document
2351      * @param travelDoc the travel document to calculate the source accounting line total for
2352      * @return the total of the source accounting lines
2353      */
2354     protected KualiDecimal getAccountingLineAmount(TravelDocument travelDoc) {
2355         KualiDecimal total = KualiDecimal.ZERO;
2356         if (travelDoc.getSourceAccountingLines() != null && !travelDoc.getSourceAccountingLines().isEmpty()) {
2357             for (TemSourceAccountingLine accountingLine : (List<TemSourceAccountingLine>)travelDoc.getSourceAccountingLines()) {
2358                 total = total.add(accountingLine.getAmount());
2359             }
2360         }
2361         return total;
2362     }
2363 
2364     /**
2365      *
2366      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getTravelDocumentNumbersByTrip(java.lang.String)
2367      */
2368     @Override
2369     public Collection<String> getApprovedTravelDocumentNumbersByTrip(String travelDocumentIdentifier) {
2370         HashMap<String,String> documentNumbersToReturn = new HashMap<String,String>();
2371 
2372         List<TravelDocument> travelDocuments = new ArrayList<TravelDocument>();
2373 
2374         TravelDocument travelDocument = getParentTravelDocument(travelDocumentIdentifier);
2375         if (ObjectUtils.isNotNull(travelDocument)) {
2376             travelDocuments.add(travelDocument);
2377         }
2378 
2379         travelDocuments.addAll(getTravelDocumentDao().findDocuments(TravelReimbursementDocument.class, travelDocumentIdentifier));
2380         travelDocuments.addAll(getTravelDocumentDao().findDocuments(TravelEntertainmentDocument.class, travelDocumentIdentifier));
2381         travelDocuments.addAll(getTravelDocumentDao().findDocuments(TravelRelocationDocument.class, travelDocumentIdentifier));
2382 
2383         for(Iterator<TravelDocument> iter = travelDocuments.iterator(); iter.hasNext();) {
2384             TravelDocument document = iter.next();
2385             if (!documentNumbersToReturn.containsKey(document.getDocumentNumber()) && isDocumentStatusValidForReconcilingCharges(document)) {
2386                 documentNumbersToReturn.put(document.getDocumentNumber(),document.getDocumentNumber());
2387             }
2388         }
2389 
2390         return documentNumbersToReturn.values();
2391     }
2392 
2393     @Override
2394     public boolean isDocumentStatusValidForReconcilingCharges(TravelDocument travelDocument) {
2395 
2396         String documentNumber = travelDocument.getDocumentNumber();
2397 
2398         if (isDocumentApprovedOrExtracted(documentNumber)) {
2399             return true;
2400         }
2401 
2402         if (travelDocument instanceof TravelAuthorizationDocument) {
2403             boolean vendorPaymentAllowedBeforeFinalAuthorization = getParameterService().getParameterValueAsBoolean(TravelAuthorizationDocument.class, TemConstants.TravelAuthorizationParameters.VENDOR_PAYMENT_ALLOWED_BEFORE_FINAL_APPROVAL_IND);
2404 
2405             if (vendorPaymentAllowedBeforeFinalAuthorization) {
2406                 return isDocumentInitiatedOrEnroute(documentNumber);
2407             }
2408         }
2409 
2410         if (travelDocument instanceof TravelReimbursementDocument) {
2411             boolean vendorPaymentAllowedBeforeFinalReimbursement = getParameterService().getParameterValueAsBoolean(TravelReimbursementDocument.class, TemConstants.TravelAuthorizationParameters.VENDOR_PAYMENT_ALLOWED_BEFORE_FINAL_APPROVAL_IND);
2412 
2413             if (vendorPaymentAllowedBeforeFinalReimbursement) {
2414                 return isDocumentInitiatedOrEnroute(documentNumber);
2415             }
2416         }
2417 
2418         return false;
2419     }
2420 
2421     /**
2422      *
2423      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#restorePerDiemProperty(org.kuali.kfs.module.tem.document.TravelDocument, java.lang.String)
2424      */
2425     @Override
2426     public void restorePerDiemProperty(TravelDocument document, String property) {
2427         try {
2428             final String[] perDiemPropertyParts = splitPerDiemProperty(property);
2429             PerDiemExpense perDiemExpense = (PerDiemExpense)ObjectUtils.getPropertyValue(document, perDiemPropertyParts[0]);
2430             final String mealName = perDiemPropertyParts[1];
2431             final boolean mealProperty = isMealProperty(mealName);
2432             final String mealSuffix = (mealProperty) ? "Value" : "";
2433             final String mealValueName = mealName+mealSuffix;
2434 
2435             KualiDecimal currentMealValue = (KualiDecimal)ObjectUtils.getPropertyValue(perDiemExpense, mealValueName);
2436             if (currentMealValue != null && currentMealValue.equals(KualiDecimal.ZERO)) {
2437                 final PerDiem perDiem = getPerDiemService().getPerDiem(perDiemExpense.getPrimaryDestinationId(), perDiemExpense.getMileageDate(), document.getEffectiveDateForPerDiem(perDiemExpense));
2438                 final KualiDecimal mealAmount = (KualiDecimal)ObjectUtils.getPropertyValue(perDiem, mealName);
2439                 final boolean prorated = mealProperty && !KfsDateUtils.isSameDay(document.getTripBegin(), document.getTripEnd()) && (KfsDateUtils.isSameDay(perDiemExpense.getMileageDate(), document.getTripBegin()) || KfsDateUtils.isSameDay(perDiemExpense.getMileageDate(), document.getTripEnd()));
2440                 if (prorated && !ObjectUtils.isNull(document.getTripType())) {
2441                     perDiemExpense.setProrated(true);
2442                     final String perDiemCalcMethod = document.getTripType().getPerDiemCalcMethod();
2443                     final Integer perDiemPercent = calculateProratePercentage(perDiemExpense, perDiemCalcMethod, document.getTripEnd());
2444                     final KualiDecimal proratedAmount = PerDiemExpense.calculateMealsAndIncidentalsProrated(mealAmount, perDiemPercent);
2445                     ObjectUtils.setObjectProperty(perDiemExpense, mealValueName, proratedAmount);
2446                 } else {
2447                     ObjectUtils.setObjectProperty(perDiemExpense, mealValueName, mealAmount);
2448                 }
2449                 if (mealProperty) {
2450                     ObjectUtils.setObjectProperty(perDiemExpense, mealName, Boolean.TRUE);
2451                 }
2452             }
2453         }
2454         catch (FormatException fe) {
2455             throw new RuntimeException("Could not set meal value on per diem expense", fe);
2456         }
2457         catch (IllegalAccessException iae) {
2458             throw new RuntimeException("Could not set meal value on per diem expense", iae);
2459         }
2460         catch (InvocationTargetException ite) {
2461             throw new RuntimeException("Could not set meal value on per diem expense", ite);
2462         }
2463         catch (NoSuchMethodException nsme) {
2464             throw new RuntimeException("Could not set meal value on per diem expense", nsme);
2465         }
2466     }
2467 
2468     /**
2469      * Determines if the given property name represents a meal on a PerDiemExpense (ie, a property with a boolean property and a "Value" property)
2470      * @param property the property to check
2471      * @return true if the property represents a field with an extra "Value" field, false otherwise
2472      */
2473     protected boolean isMealProperty(String property) {
2474         return StringUtils.equals(property, TemPropertyConstants.BREAKFAST) || StringUtils.equals(property, TemPropertyConstants.LUNCH) || StringUtils.equals(property, TemPropertyConstants.DINNER) || StringUtils.equals(property, TemPropertyConstants.INCIDENTALS);
2475     }
2476 
2477     /**
2478      * Splits a property into the per diem part and the property of the per diem expense we should update
2479      * @param property the property to split
2480      * @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
2481      */
2482     protected String[] splitPerDiemProperty(String property) {
2483         final String deDocumentedProperty = property.replace(KFSPropertyConstants.DOCUMENT+".", KFSConstants.EMPTY_STRING);
2484         final int lastDivider = deDocumentedProperty.lastIndexOf('.');
2485         final String perDiemPart = deDocumentedProperty.substring(0, lastDivider);
2486         final String mealPart = deDocumentedProperty.substring(lastDivider+1);
2487         return new String[] { perDiemPart, mealPart };
2488     }
2489 
2490     /**
2491      * Looks up the document with the progenitor document for the trip
2492      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getRootTravelDocumentWithoutWorkflowDocument(java.lang.String)
2493      */
2494     @Override
2495     public TravelDocument getRootTravelDocumentWithoutWorkflowDocument(String travelDocumentIdentifier) {
2496         Map<String, Object> fieldValues = new HashMap<String, Object>();
2497         fieldValues.put(TemPropertyConstants.TRAVEL_DOCUMENT_IDENTIFIER, travelDocumentIdentifier);
2498         fieldValues.put(TemPropertyConstants.TRIP_PROGENITOR, new Boolean(true));
2499         for (String documentType : getTravelDocumentTypesToCheck()) {
2500             final Class<? extends TravelDocument> docClazz = getTravelDocumentForType(documentType);
2501             Collection<TravelDocument> matchingDocs = (Collection<TravelDocument>)getBusinessObjectService().findMatching(docClazz, fieldValues);
2502             if (matchingDocs != null && !matchingDocs.isEmpty()) {
2503                 List<TravelDocument> foundDocs = new ArrayList<TravelDocument>();
2504                 foundDocs.addAll(matchingDocs);
2505                 return foundDocs.get(0);
2506             }
2507         }
2508         return null;
2509     }
2510 
2511     /**
2512      * HEY EVERYONE! BIG CUSTOMIZATION OPPORTUNITY!
2513      * This method returns an ordered list of where to look for progenitor documents.  The order is based on my total guess of which
2514      * 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
2515      * be first.  Anyhow, please feel free to rearrange this list as seems most helpful to you
2516      * @return a List of the document types to look for root documents in - in which order
2517      */
2518     protected List<String> getTravelDocumentTypesToCheck() {
2519         List<String> documentTypes = new ArrayList<String>();
2520         documentTypes.add(TemConstants.TravelDocTypes.TRAVEL_AUTHORIZATION_DOCUMENT);
2521         documentTypes.add(TemConstants.TravelDocTypes.TRAVEL_ENTERTAINMENT_DOCUMENT);
2522         documentTypes.add(TemConstants.TravelDocTypes.TRAVEL_RELOCATION_DOCUMENT);
2523         documentTypes.add(TemConstants.TravelDocTypes.TRAVEL_REIMBURSEMENT_DOCUMENT);
2524         return documentTypes;
2525     }
2526 
2527     /**
2528      * Looks up the class associated with the given document type to check
2529      * @param documentType the document type name to find a class for
2530      * @return the class of that document type
2531      */
2532     protected Class<? extends TravelDocument> getTravelDocumentForType(String documentType) {
2533         return (Class<TravelDocument>)getDataDictionaryService().getDocumentClassByTypeName(documentType);
2534     }
2535 
2536     /**
2537      * 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
2538      * which have the same chart - account - sub-acount.
2539      * @param originalAccountingLines the List of accounting lines to smoosh
2540      * @return the smooshed accounting lines
2541      */
2542     @Override
2543     public List<TemSourceAccountingLine> smooshAccountingLinesToSubAccount(List<TemSourceAccountingLine> originalAccountingLines) {
2544        final Map<SmooshLineKey, KualiDecimal> smooshLines =  smooshLinesToMap(originalAccountingLines);
2545        final List<TemSourceAccountingLine> unsmooshedLines = raiseMapToLines(smooshLines);
2546        return unsmooshedLines;
2547     }
2548 
2549     /**
2550      * Smooshes the lines into a Map
2551      * @param accountingLines the accounting lines to smoosh
2552      * @return the Map of smooshed lines
2553      */
2554     protected Map<SmooshLineKey, KualiDecimal> smooshLinesToMap(List<TemSourceAccountingLine> accountingLines) {
2555         Map<SmooshLineKey, KualiDecimal> smooshLines = new HashMap<SmooshLineKey, KualiDecimal>();
2556         for (TemSourceAccountingLine line : accountingLines) {
2557             final SmooshLineKey key = new SmooshLineKey(line);
2558             if (smooshLines.containsKey(key)) {
2559                 KualiDecimal currAmount = smooshLines.get(key);
2560                 KualiDecimal newAmount = currAmount.add(line.getAmount());
2561                 smooshLines.put(key, newAmount);
2562             } else {
2563                 smooshLines.put(key, line.getAmount());
2564             }
2565         }
2566         return smooshLines;
2567     }
2568 
2569     /**
2570      * 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
2571      * @param smooshLineMap the Map to turn back into accounting lines
2572      * @return the un-smooshed accounting lines.  Yeah, I like that verb better too
2573      */
2574     protected List<TemSourceAccountingLine> raiseMapToLines(Map<SmooshLineKey, KualiDecimal> smooshLineMap) {
2575         List<TemSourceAccountingLine> raisedLines = new ArrayList<TemSourceAccountingLine>();
2576         for (SmooshLineKey key : smooshLineMap.keySet()) {
2577             final TemSourceAccountingLine line = convertKeyAndAmountToLine(key, smooshLineMap.get(key));
2578             raisedLines.add(line);
2579         }
2580         return raisedLines;
2581     }
2582 
2583     /**
2584      * Converts a SmooshLineKey and an amount into a real - though somewhat less informative - accounting line
2585      * @param key the key
2586      * @param amount the amount
2587      * @return the reconstituted accounting line.  I like that verb too.
2588      */
2589     protected TemSourceAccountingLine convertKeyAndAmountToLine(SmooshLineKey key, KualiDecimal amount) {
2590         TemSourceAccountingLine line = new TemSourceAccountingLine();
2591         line.setChartOfAccountsCode(key.getChartOfAccountsCode());
2592         line.setAccountNumber(key.getAccountNumber());
2593         line.setSubAccountNumber(key.getSubAccountNumber());
2594         line.setAmount(amount);
2595         return line;
2596     }
2597 
2598     /**
2599      * Hash key of lines we want to smoosh
2600      */
2601     protected class SmooshLineKey {
2602         protected String chartOfAccountsCode;
2603         protected String accountNumber;
2604         protected String subAccountNumber;
2605 
2606         public SmooshLineKey(TemSourceAccountingLine accountingLine) {
2607             this.chartOfAccountsCode = accountingLine.getChartOfAccountsCode();
2608             this.accountNumber = accountingLine.getAccountNumber();
2609             this.subAccountNumber = accountingLine.getSubAccountNumber();
2610         }
2611 
2612         public String getChartOfAccountsCode() {
2613             return chartOfAccountsCode;
2614         }
2615 
2616         public String getAccountNumber() {
2617             return accountNumber;
2618         }
2619 
2620         public String getSubAccountNumber() {
2621             return subAccountNumber;
2622         }
2623 
2624         @Override
2625         public int hashCode() {
2626             HashCodeBuilder hcb = new HashCodeBuilder();
2627             hcb.append(getChartOfAccountsCode());
2628             hcb.append(getAccountNumber());
2629             hcb.append(getSubAccountNumber());
2630             return hcb.toHashCode();
2631         }
2632 
2633         @Override
2634         public boolean equals(Object obj) {
2635             if (!(obj instanceof SmooshLineKey) || obj == null) {
2636                 return false;
2637             }
2638             final SmooshLineKey golyadkin = (SmooshLineKey)obj;
2639             EqualsBuilder eb = new EqualsBuilder();
2640             eb.append(getChartOfAccountsCode(), golyadkin.getChartOfAccountsCode());
2641             eb.append(getAccountNumber(), golyadkin.getAccountNumber());
2642             eb.append(getSubAccountNumber(), golyadkin.getSubAccountNumber());
2643             return eb.isEquals();
2644         }
2645     }
2646 
2647     /**
2648      * Parses the value of url.document.travelRelocation.agencySites and turns those into links
2649      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#getAgencyLinks(org.kuali.kfs.module.tem.document.TravelDocument)
2650      */
2651     @Override
2652     public List<LinkField> getAgencyLinks(TravelDocument travelDocument) {
2653         List<LinkField> agencyLinks = new ArrayList<LinkField>();
2654         if (getConfigurationService().getPropertyValueAsBoolean(TemKeyConstants.ENABLE_AGENCY_SITES_URL)) {
2655             final String agencySitesURL = getConfigurationService().getPropertyValueAsString(TemKeyConstants.AGENCY_SITES_URL);
2656             final String target = "_blank";
2657             if(!StringUtils.isEmpty(agencySitesURL)){
2658                 String[] sites = agencySitesURL.split(";");
2659                 for (String site : sites){
2660                     String[] siteInfo = site.split("=");
2661                     String url = customizeAgencyLink(travelDocument, siteInfo[0], siteInfo[1]);
2662                     final String prefixedUrl = prefixUrl(url);
2663                     LinkField link = new LinkField();
2664                     link.setHrefText(prefixedUrl);
2665                     link.setTarget(target);
2666                     link.setLinkLabel(siteInfo[0]);
2667                     agencyLinks.add(link);
2668                 }
2669             }
2670         }
2671         return agencyLinks;
2672     }
2673 
2674     /**
2675      * In the default version, checks if the "config.document.travelRelocation.agencySites.include.tripId" property is true and if it is, just dumbly
2676      * 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.
2677      * @see org.kuali.kfs.module.tem.document.service.TravelDocumentService#customizeAgencyLink(org.kuali.kfs.module.tem.document.TravelDocument, java.lang.String, java.lang.String)
2678      */
2679     @Override
2680     public String customizeAgencyLink(TravelDocument travelDocument, String agencyName, String link) {
2681         final boolean passTrip = getConfigurationService().getPropertyValueAsBoolean(TemKeyConstants.PASS_TRIP_ID_TO_AGENCY_SITES);
2682         if (!passTrip ||  StringUtils.isBlank(travelDocument.getTravelDocumentIdentifier())) {
2683             return link; // nothing to add
2684         }
2685 
2686         if (travelDocument instanceof TravelAuthorizationDocument) {
2687             final boolean vendorPaymentAllowedBeforeFinal = getParameterService().getParameterValueAsBoolean(TravelAuthorizationDocument.class, TemConstants.TravelAuthorizationParameters.VENDOR_PAYMENT_ALLOWED_BEFORE_FINAL_APPROVAL_IND);
2688             if (!vendorPaymentAllowedBeforeFinal) {
2689                 return link;
2690             }
2691         }
2692         final String linkWithTripId = link+"?tripId="+travelDocument.getTravelDocumentIdentifier();
2693         return linkWithTripId;
2694     }
2695 
2696     /**
2697      * Makes sure that url starts with https
2698      * @param url the url to prefix as needed
2699      * @return the url prefixed by protocol
2700      */
2701     protected String prefixUrl(String url) {
2702         String prefixedUrl = url;
2703         if (!prefixedUrl.startsWith("http")) {
2704             prefixedUrl = "https://"+prefixedUrl;
2705         }
2706         return prefixedUrl;
2707     }
2708 
2709     /**
2710      * @see org.kuali.kfs.module.tem.document.service.TravelArrangerDocumentService#isInitiatorTraveler(TravelDocument)
2711      */
2712     @Override
2713     public boolean isInitiatorTraveler(TravelDocument travelDoc) {
2714         String initiatorId = travelDoc.getDocumentHeader().getWorkflowDocument().getInitiatorPrincipalId();
2715         String travelerId = travelDoc.getTraveler().getPrincipalId();
2716         boolean is = travelerId != null && initiatorId.equals(travelerId);
2717         return is;
2718     }
2719 
2720     /**
2721      * @see org.kuali.kfs.module.tem.document.service.TravelArrangerDocumentService#requiresTravelerApproval(TravelAuthorizationDocument)
2722      */
2723     @Override
2724     public boolean requiresTravelerApproval(TravelAuthorizationDocument taDoc) {
2725         // If there's travel advances, route to traveler if necessary
2726         boolean require = taDoc.requiresTravelAdvanceReviewRouting();
2727         require &= !taDoc.getTravelAdvance().getTravelAdvancePolicy();
2728 
2729         return require;
2730     }
2731 
2732     /**
2733      * @see org.kuali.kfs.module.tem.document.service.TravelArrangerDocumentService#requiresTravelerApproval(TEMReimbursementDocument)
2734      */
2735     @Override
2736     public boolean requiresTravelerApproval(TEMReimbursementDocument trDoc) {
2737         String travelerTypeCode = trDoc.getTraveler().getTravelerTypeCode();
2738         if (parameterService.getParameterValuesAsString(TemParameterConstants.TEM_DOCUMENT.class, TravelParameters.NON_EMPLOYEE_TRAVELER_TYPE_CODES).contains(travelerTypeCode)) {
2739             return false;
2740         }
2741 
2742         // no need to route back to traveler if s/he is the initiator
2743         return !isInitiatorTraveler(trDoc);
2744     }
2745 
2746     /**
2747      * @return the system-ste implementation of the AccountsReceivableModuleService
2748      */
2749     public AccountsReceivableModuleService getAccountsReceivableModuleService() {
2750         if (accountsReceivableModuleService == null) {
2751             accountsReceivableModuleService = SpringContext.getBean(AccountsReceivableModuleService.class);
2752         }
2753         return accountsReceivableModuleService;
2754     }
2755 
2756     public TravelAuthorizationService getTravelAuthorizationService() {
2757         return travelAuthorizationService;
2758     }
2759 
2760     public void setTravelAuthorizationService(TravelAuthorizationService travelAuthorizationService) {
2761         this.travelAuthorizationService = travelAuthorizationService;
2762     }
2763 
2764     public PerDiemService getPerDiemService() {
2765         return perDiemService;
2766     }
2767 
2768     public void setPerDiemService(PerDiemService perDiemService) {
2769         this.perDiemService = perDiemService;
2770     }
2771 
2772     public List<String> getGroupTravelerColumns() {
2773         return groupTravelerColumns;
2774     }
2775 
2776     public void setGroupTravelerColumns(List<String> groupTravelerColumns) {
2777         this.groupTravelerColumns = groupTravelerColumns;
2778     }
2779 
2780     public TravelExpenseService getTravelExpenseService() {
2781         return travelExpenseService;
2782     }
2783 
2784     public void setTravelExpenseService(TravelExpenseService travelExpenseService) {
2785         this.travelExpenseService = travelExpenseService;
2786     }
2787 
2788     public NoteService getNoteService() {
2789         return noteService;
2790     }
2791 
2792     public void setNoteService(NoteService noteService) {
2793         this.noteService = noteService;
2794     }
2795 
2796     public TravelService getTravelService() {
2797         return travelService;
2798     }
2799 
2800     public void setTravelService(TravelService travelService) {
2801         this.travelService = travelService;
2802     }
2803 
2804     public MileageRateService getMileageRateService() {
2805         return mileageRateService;
2806     }
2807 
2808     public void setMileageRateService(MileageRateService mileageRateService) {
2809         this.mileageRateService = mileageRateService;
2810     }
2811 
2812     public void setDocumentService(DocumentService documentService) {
2813         this.documentService = documentService;
2814     }
2815 
2816     protected DocumentService getDocumentService() {
2817         return documentService;
2818     }
2819 
2820     public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
2821         this.dataDictionaryService = dataDictionaryService;
2822     }
2823 
2824     protected DataDictionaryService getDataDictionaryService() {
2825         return dataDictionaryService;
2826     }
2827 
2828     public void setDateTimeService(final DateTimeService dateTimeService) {
2829         this.dateTimeService = dateTimeService;
2830     }
2831 
2832     protected DateTimeService getDateTimeService() {
2833         return dateTimeService;
2834     }
2835 
2836     public void setTravelDocumentDao(final TravelDocumentDao travelDocumentDao) {
2837         this.travelDocumentDao = travelDocumentDao;
2838     }
2839 
2840     protected TravelDocumentDao getTravelDocumentDao() {
2841         return travelDocumentDao;
2842     }
2843 
2844     public void setBusinessObjectService(BusinessObjectService businessObjectService) {
2845         this.businessObjectService = businessObjectService;
2846     }
2847 
2848     protected BusinessObjectService getBusinessObjectService() {
2849         return businessObjectService;
2850     }
2851 
2852     public ParameterService getParameterService() {
2853         return parameterService;
2854     }
2855 
2856     public void setParameterService(ParameterService parameterService) {
2857         this.parameterService = parameterService;
2858     }
2859 
2860     public AccountingDocumentRelationshipService getAccountingDocumentRelationshipService() {
2861         return accountingDocumentRelationshipService;
2862     }
2863 
2864     public void setAccountingDocumentRelationshipService(AccountingDocumentRelationshipService accountingDocumentRelationshipService) {
2865         this.accountingDocumentRelationshipService = accountingDocumentRelationshipService;
2866     }
2867 
2868     public TemRoleService getTemRoleService() {
2869         return temRoleService;
2870     }
2871 
2872     public void setTemRoleService(TemRoleService temRoleService) {
2873         this.temRoleService = temRoleService;
2874     }
2875 
2876     protected ConfigurationService getConfigurationService() {
2877         return configurationService;
2878     }
2879 
2880     public void setConfigurationService(ConfigurationService configurationService) {
2881         this.configurationService = configurationService;
2882     }
2883 
2884     public StateService getStateService() {
2885         return stateService;
2886     }
2887 
2888     public void setStateService(StateService stateService) {
2889         this.stateService = stateService;
2890     }
2891 
2892     /**
2893      * Gets the universityDateService attribute.
2894      * @return Returns the universityDateService.
2895      */
2896     public UniversityDateService getUniversityDateService() {
2897         return universityDateService;
2898     }
2899 
2900     /**
2901      * Sets the universityDateService attribute value.
2902      * @param universityDateService The universityDateService to set.
2903      */
2904     public void setUniversityDateService(UniversityDateService universityDateService) {
2905         this.universityDateService = universityDateService;
2906     }
2907 
2908     public List<String> getDefaultAcceptableFileExtensions() {
2909         return defaultAcceptableFileExtensions;
2910     }
2911 
2912     public void setDefaultAcceptableFileExtensions(final List<String> defaultAcceptableFileExtensions) {
2913         this.defaultAcceptableFileExtensions = defaultAcceptableFileExtensions;
2914     }
2915 
2916     public void setCsvRecordFactory(final CsvRecordFactory<GroupTravelerCsvRecord> recordFactory) {
2917         this.csvRecordFactory = recordFactory;
2918     }
2919 
2920     public CsvRecordFactory<GroupTravelerCsvRecord> getCsvRecordFactory() {
2921         return this.csvRecordFactory;
2922     }
2923 }