/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.module.tem.service.impl; import java.lang.reflect.InvocationTargetException; import java.text.ParseException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.fp.businessobject.TravelExpenseTypeCode; import org.kuali.kfs.module.tem.TemConstants; import org.kuali.kfs.module.tem.TemPropertyConstants; import org.kuali.kfs.module.tem.TemConstants.AgencyStagingDataErrorCodes; import org.kuali.kfs.module.tem.TemConstants.CreditCardStagingDataErrorCodes; import org.kuali.kfs.module.tem.TemConstants.ExpenseImportTypes; import org.kuali.kfs.module.tem.TemConstants.ExpenseTypeMetaCategory; import org.kuali.kfs.module.tem.TemConstants.ReconciledCodes; import org.kuali.kfs.module.tem.businessobject.ActualExpense; import org.kuali.kfs.module.tem.businessobject.AgencyStagingData; import org.kuali.kfs.module.tem.businessobject.CreditCardAgency; import org.kuali.kfs.module.tem.businessobject.CreditCardStagingData; import org.kuali.kfs.module.tem.businessobject.ExpenseType; import org.kuali.kfs.module.tem.businessobject.ExpenseTypeObjectCode; import org.kuali.kfs.module.tem.businessobject.HistoricalTravelExpense; import org.kuali.kfs.module.tem.businessobject.OtherExpense; import org.kuali.kfs.module.tem.businessobject.TemExpense; import org.kuali.kfs.module.tem.businessobject.TripAccountingInformation; import org.kuali.kfs.module.tem.dataaccess.ExpenseTypeObjectCodeDao; import org.kuali.kfs.module.tem.document.TravelDocument; import org.kuali.kfs.module.tem.document.web.struts.TravelFormBase; import org.kuali.kfs.module.tem.service.TemExpenseService; import org.kuali.kfs.module.tem.service.TravelExpenseService; import org.kuali.kfs.module.tem.service.TravelService; import org.kuali.kfs.sys.KFSPropertyConstants; import org.kuali.kfs.sys.context.SpringContext; import org.kuali.rice.core.api.datetime.DateTimeService; import org.kuali.rice.core.api.util.type.KualiDecimal; import org.kuali.rice.kew.api.exception.WorkflowException; import org.kuali.rice.kim.api.KimConstants; import org.kuali.rice.kim.api.identity.Person; import org.kuali.rice.kns.service.DocumentHelperService; import org.kuali.rice.kns.util.KNSGlobalVariables; import org.kuali.rice.kns.web.struts.form.KualiForm; import org.kuali.rice.krad.service.BusinessObjectService; import org.kuali.rice.krad.service.DocumentService; import org.kuali.rice.krad.util.KRADConstants; import org.kuali.rice.krad.util.ObjectUtils; import org.springframework.transaction.annotation.Transactional; /** * Service for handling travel expenses * * @see org.kuali.kfs.module.tem.document.validation.impl.AgencyStagingDataRule */ @Transactional public class TravelExpenseServiceImpl implements TravelExpenseService { org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(TravelExpenseServiceImpl.class); protected BusinessObjectService businessObjectService; protected DateTimeService dateTimeService; protected ExpenseTypeObjectCodeDao expenseTypeObjectCodeDao; protected DocumentHelperService documentHelperService; protected TravelService travelService; @Override public ExpenseTypeObjectCode getExpenseType(String expense, String documentType, String tripType, String travelerType) { if (StringUtils.isBlank(expense)) { throw new IllegalArgumentException("Expense Type Code cannot be blank"); } final Set<String> parentDocumentTypes = getTravelService().getParentDocumentTypeNames(documentType); List<ExpenseTypeObjectCode> expenseTypeObjectCodes = getExpenseTypeObjectCodeDao().findMatchingExpenseTypeObjectCodes(expense, parentDocumentTypes, tripType, travelerType); if (expenseTypeObjectCodes == null || expenseTypeObjectCodes.isEmpty()) { return null; } Collections.sort(expenseTypeObjectCodes, getExpenseTypeObjectCodeComparator()); final ExpenseTypeObjectCode chosenExpenseTypeObjectCode = expenseTypeObjectCodes.get(0); if (LOG.isDebugEnabled()) { LOG.debug("I choose you, "+chosenExpenseTypeObjectCode.toString()); } return chosenExpenseTypeObjectCode; } /** * @return the comparator that will sort expense type object codes, such that the top one will be selected as the "most" correct */ protected Comparator<ExpenseTypeObjectCode> getExpenseTypeObjectCodeComparator() { return new ExpenseTypeObjectCodeComparatorByHierarchyLogic(); } /** * This wise child knows how to sort ExpenseTypeObjectCode records to find the one which should be most associated with a set of given conditions. Basically, it sorts with more specific records being * higher ranked than less specific records. It checks expense type code; then document type level (TAA is more specific than TRV, so should be higher); then traveler type (something specific ranks higher than ALL); and finally trip type (again, something specific ranks higher than ALL) */ protected class ExpenseTypeObjectCodeComparatorByHierarchyLogic implements Comparator<ExpenseTypeObjectCode> { @Override public int compare(ExpenseTypeObjectCode dora, ExpenseTypeObjectCode nora) { if (!StringUtils.equals(dora.getExpenseTypeCode(), nora.getExpenseTypeCode())) { return dora.getExpenseTypeCode().compareTo(nora.getExpenseTypeCode()); } if (!StringUtils.equals(dora.getDocumentTypeName(), nora.getDocumentTypeName())) { final int doraDocLevel = getLevelForDocumentType(dora.getDocumentTypeName()); final int noraDocLevel = getLevelForDocumentType(nora.getDocumentTypeName()); final int delta = doraDocLevel - noraDocLevel; return delta; } if (!StringUtils.equals(dora.getTravelerTypeCode(), nora.getTravelerTypeCode())) { if (TemConstants.ALL_EXPENSE_TYPE_OBJECT_CODE_TRAVELER_TYPE.equals(dora.getTravelerTypeCode())) { return 1; // advantage: nora } if (TemConstants.ALL_EXPENSE_TYPE_OBJECT_CODE_TRAVELER_TYPE.equals(nora.getTravelerTypeCode())) { return -1; // advantage: dora } // we're still here? That's...weird (we shouldn't have both EMP and NON records). Let's have trip type figure it out for us } if (!StringUtils.equals(dora.getTripTypeCode(), nora.getTripTypeCode())) { if (TemConstants.ALL_EXPENSE_TYPE_OBJECT_CODE_TRIP_TYPE.equals(dora.getTripTypeCode())) { return 1; // advantage: nora } if (TemConstants.ALL_EXPENSE_TYPE_OBJECT_CODE_TRAVELER_TYPE.equals(nora.getTripTypeCode())) { return -1; // advantage: dora } if (StringUtils.isBlank(dora.getTripTypeCode())) { return 1; // advantage: nora. Though how did we get here? } return dora.getTripTypeCode().compareTo(nora.getTripTypeCode()); // another place we shouldn't ever really get to } return 0; // odd that we got here, but whichever } /** * Returns the branch level of the given document type * @param documentType the document type to find a branch level for * @return the branch level, with TT being high and doc types which descend from TT having lower levels */ protected int getLevelForDocumentType(String documentType) { if (TemConstants.TravelDocTypes.TEM_TRANSACTIONAL_DOCUMENT.equals(documentType)) { return 3; } else if (TemConstants.TravelDocTypes.TRAVEL_TRANSACTIONAL_DOCUMENT.equals(documentType) || TemConstants.TravelDocTypes.TRAVEL_ENTERTAINMENT_DOCUMENT.equals(documentType) || TemConstants.TravelDocTypes.TRAVEL_RELOCATION_DOCUMENT.equals(documentType)) { return 2; } else if (TemConstants.TravelDocTypes.TRAVEL_AUTHORIZATION_DOCUMENT.equals(documentType) || TemConstants.TravelDocTypes.TRAVEL_REIMBURSEMENT_DOCUMENT.equals(documentType)) { return 1; } return 0; // should be TAA and TAC only here } } @Override public ExpenseTypeObjectCode getExpenseTypeObjectCode(String travelExpenseCode, String documentNumber) { final TravelDocument document = retrieveTravelDocument(documentNumber); if (document != null) { final String documentType = document.getFinancialDocumentTypeCode(); final String tripType = document.getTripTypeCode(); final String travelerType = document.getTraveler().getTravelerTypeCode(); return getExpenseType(travelExpenseCode, documentType, tripType, travelerType); } return null; } /** * Attempts to retrieve the TravelDocument in operation - first from the form, then by looking up via document service * @param documentNumber the document number of the document to attempt retrieval of * @return the retrieved document, or null if retrieval was unsucessful */ protected TravelDocument retrieveTravelDocument(String documentNumber) { // first, we'll look in the form KualiForm form = KNSGlobalVariables.getKualiForm(); if (form instanceof TravelFormBase) { final TravelDocument document = ((TravelFormBase)form).getTravelDocument(); if (!StringUtils.isBlank(document.getDocumentNumber()) && document.getDocumentNumber().equals(documentNumber)) { return document; } } // still here? Let's look it up try { final TravelDocument document = (TravelDocument)getDocumentService().getByDocumentHeaderIdSessionless(documentNumber); return document; } catch (WorkflowException we) { throw new RuntimeException("Could not retrieve document "+documentNumber, we); } } /** * * @see org.kuali.kfs.module.tem.service.TravelExpenseService#getExpenseTypesForDocument(java.lang.String, java.lang.String, java.lang.String, boolean) */ @Override public List<ExpenseType> getExpenseTypesForDocument(String documentTypeName, String tripType, String travelerType, boolean groupOnly) { final Set<String> documentTypesForSearch = getTravelService().getParentDocumentTypeNames(documentTypeName); final List<ExpenseTypeObjectCode> expenseTypeObjectCodes = getExpenseTypeObjectCodeDao().findMatchingExpenseTypesObjectCodes(documentTypesForSearch, tripType, travelerType); Set<String> expenseTypeCodes = new HashSet<String>(); List<ExpenseType> expenseTypes = new ArrayList<ExpenseType>(); for (ExpenseTypeObjectCode expenseTypeObjectCode : expenseTypeObjectCodes) { if (!expenseTypeCodes.contains(expenseTypeObjectCode.getExpenseTypeCode())) { if (!groupOnly || (groupOnly && expenseTypeObjectCode.getExpenseType().isGroupTravel())) { expenseTypeObjectCode.refreshReferenceObject(TemPropertyConstants.EXPENSE_TYPE); expenseTypes.add(expenseTypeObjectCode.getExpenseType()); expenseTypeCodes.add(expenseTypeObjectCode.getExpenseTypeCode()); } } } return expenseTypes; } /** * Comparator which compares expense types by their code */ protected class ExpenseTypeComparatorByCode implements Comparator<ExpenseType> { @Override public int compare(ExpenseType lava, ExpenseType kusha) { if (StringUtils.equals(lava.getCode(), kusha.getCode())) { return 0; } else if (lava.getCode() == null && !StringUtils.isBlank(kusha.getCode())) { return 1; } else { return lava.getCode().compareTo(kusha.getCode()); } } } @Override public HistoricalTravelExpense createHistoricalTravelExpense(AgencyStagingData agency) { HistoricalTravelExpense expense = new HistoricalTravelExpense(); try { expense.setImportDate(dateTimeService.convertToSqlDate(agency.getProcessingTimestamp())); } catch (ParseException e) { throw new RuntimeException("Unable to convert timestamp to date " + e.getMessage()); } CreditCardAgency ccAgency =agency.getCreditCardAgency(); expense.setCreditCardAgency(ccAgency); expense.setCreditCardOrAgencyCode(ccAgency.getCreditCardOrAgencyCode()); final ExpenseType expenseType = getDefaultExpenseTypeForCategory(agency.getExpenseTypeCategory()); if (expenseType != null) { expense.setTravelExpenseTypeCode(expenseType.getCode()); } expense.setTravelCompany(agency.getMerchantName()); expense.setAmount(agency.getTripExpenseAmount()); expense.setTransactionPostingDate(agency.getTransactionPostingDate()); expense.setAgencyStagingDataId(agency.getId()); expense.setTripId(agency.getTripId()); expense.setProfileId(agency.getTemProfileId()); expense.setReconciled(ReconciledCodes.UNRECONCILED); return expense; } /** * * This method returns a {@link TravelExpenseTypeCode}. This is needed to convert from the code to the name for imports by traveler. * @param expenseType * @return */ protected TravelExpenseTypeCode getTravelExpenseTypeCode(String expenseType) { Map<String, String> primaryKeys = new HashMap<String, String>(); primaryKeys.put(KFSPropertyConstants.CODE, expenseType); return getBusinessObjectService().findByPrimaryKey(TravelExpenseTypeCode.class, primaryKeys); } /** * @see org.kuali.kfs.module.tem.service.TravelExpenseService#createHistoricalTravelExpense(org.kuali.kfs.module.tem.businessobject.CreditCardStagingData) */ @Override public HistoricalTravelExpense createHistoricalTravelExpense(CreditCardStagingData creditCard) { HistoricalTravelExpense expense = new HistoricalTravelExpense(); try { expense.setImportDate(dateTimeService.convertToSqlDate(creditCard.getProcessingTimestamp())); } catch (ParseException e) { throw new RuntimeException("Unable to convert timestamp to date " + e.getMessage()); } CreditCardAgency ccAgency = creditCard.getCreditCardAgency(); expense.setCreditCardOrAgencyCode(ccAgency.getCreditCardOrAgencyCode()); expense.setTravelExpenseTypeCode(creditCard.getExpenseTypeCode()); expense.setDescription(creditCard.getExpenseTypeCode()); expense.setTravelCompany(creditCard.getMerchantName()); expense.setAmount(creditCard.getTransactionAmount()); expense.setTransactionPostingDate(creditCard.getTransactionDate()); expense.setCreditCardStagingDataId(creditCard.getId()); expense.setProfileId(creditCard.getTemProfileId()); expense.setLocation(creditCard.getLocation()); expense.setReconciled(ReconciledCodes.UNRECONCILED); return expense; } @Override public HistoricalTravelExpense createHistoricalTravelExpense(AgencyStagingData agency, CreditCardStagingData creditCard, ExpenseTypeObjectCode travelExpenseType) { HistoricalTravelExpense expense = createHistoricalTravelExpense(agency); expense.setLocation(creditCard.getLocation()); expense.setCreditCardStagingDataId(creditCard.getId()); expense.setReconciled(ReconciledCodes.RECONCILED); expense.setReconciliationDate(getDateTimeService().getCurrentSqlDate()); return expense; } @Override public List<AgencyStagingData> retrieveValidAgencyData() { Map<String,String> criteria = new HashMap<String,String>(1); criteria.put(TemPropertyConstants.AGENCY_ERROR_CODE, AgencyStagingDataErrorCodes.AGENCY_NO_ERROR); criteria.put(TemPropertyConstants.ACTIVE_IND, TemConstants.YES); List<AgencyStagingData> agencyData = (List<AgencyStagingData>) getBusinessObjectService().findMatching(AgencyStagingData.class, criteria); return agencyData; } @Override public List<AgencyStagingData> retrieveValidAgencyDataByImportType(String importBy) { Map<String,String> criteria = new HashMap<String,String>(2); criteria.put(TemPropertyConstants.AGENCY_ERROR_CODE, AgencyStagingDataErrorCodes.AGENCY_NO_ERROR); criteria.put(TemPropertyConstants.IMPORT_BY, importBy); List<AgencyStagingData> agencyData = (List<AgencyStagingData>) getBusinessObjectService().findMatching(AgencyStagingData.class, criteria); return agencyData; } @Override public List<CreditCardStagingData> retrieveValidCreditCardData() { Map<String,String> criteria = new HashMap<String,String>(1); criteria.put(TemPropertyConstants.AGENCY_ERROR_CODE, CreditCardStagingDataErrorCodes.CREDIT_CARD_NO_ERROR); criteria.put(TemPropertyConstants.IMPORT_BY, ExpenseImportTypes.IMPORT_BY_TRAVELLER); List<CreditCardStagingData> creditCardData = (List<CreditCardStagingData>) getBusinessObjectService().findMatching(CreditCardStagingData.class, criteria); return creditCardData; } @Override public CreditCardStagingData findImportedCreditCardExpense(KualiDecimal amount, String itineraryNumber) { Map<String,Object> criteria = new HashMap<String,Object>(3); criteria.put(TemPropertyConstants.TRANSACTION_AMOUNT, amount); criteria.put(TemPropertyConstants.ITINERARY_NUMBER, itineraryNumber); criteria.put(TemPropertyConstants.AGENCY_ERROR_CODE, CreditCardStagingDataErrorCodes.CREDIT_CARD_NO_ERROR); List<CreditCardStagingData> creditCardData = (List<CreditCardStagingData>) getBusinessObjectService().findMatching(CreditCardStagingData.class, criteria); if (ObjectUtils.isNotNull(creditCardData) && creditCardData.size() > 0) { return creditCardData.get(0); } return null; } @Override public CreditCardStagingData findImportedCreditCardExpense(KualiDecimal amount, String ticketNumber, String serviceFeeNumber) { Map<String,Object> criteria = new HashMap<String,Object>(4); criteria.put(TemPropertyConstants.TRANSACTION_AMOUNT, amount); criteria.put(TemPropertyConstants.TICKET_NUMBER, ticketNumber); criteria.put(TemPropertyConstants.SERVICE_FEE_NUMBER, serviceFeeNumber); criteria.put(TemPropertyConstants.AGENCY_ERROR_CODE, CreditCardStagingDataErrorCodes.CREDIT_CARD_NO_ERROR); List<CreditCardStagingData> creditCardData = (List<CreditCardStagingData>) getBusinessObjectService().findMatching(CreditCardStagingData.class, criteria); if (ObjectUtils.isNotNull(creditCardData) && creditCardData.size() > 0) { return creditCardData.get(0); } return null; } /** * This method should only be called when adding an actual expense or detail * @see org.kuali.kfs.module.tem.service.TravelExpenseService#updateTaxabilityOfActualExpense(org.kuali.kfs.module.tem.businessobject.ActualExpense, org.kuali.kfs.module.tem.document.TravelDocument, org.kuali.rice.kim.api.identity.Person) */ @Override public void updateTaxabilityOfActualExpense(ActualExpense actualExpense, TravelDocument document, Person currentUser) { if (!ObjectUtils.isNull(actualExpense.getExpenseTypeObjectCode())) { // don't have an expense type object code? then why are we bothering? // 1. if the given user cannot change the taxability then we should actually look at it Map<String, String> permissionDetails = new HashMap<String, String>(); permissionDetails.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, document.getDocumentTypeName()); permissionDetails.put(KimConstants.AttributeConstants.EDIT_MODE, TemConstants.EditModes.EXPENSE_TAXABLE_MODE); final boolean canEditTaxable = getDocumentHelperService().getDocumentAuthorizer(document).isAuthorizedByTemplate(document, KRADConstants.KNS_NAMESPACE, KimConstants.PermissionTemplateNames.USE_TRANSACTIONAL_DOCUMENT, currentUser.getPrincipalId(), permissionDetails, Collections.<String, String>emptyMap()); if (!canEditTaxable) { actualExpense.setTaxable(actualExpense.getExpenseTypeObjectCode().isTaxable()); } } } /** * Does BusinessObjectService lookup - pretty simple * @see org.kuali.kfs.module.tem.service.TravelExpenseService#getDefaultExpenseTypeForCategory(org.kuali.kfs.module.tem.TemConstants.ExpenseTypeMetaCategory) */ @Override public ExpenseType getDefaultExpenseTypeForCategory(ExpenseTypeMetaCategory category) { Map<String, Object> fieldValues = new HashMap<String, Object>(); fieldValues.put(TemPropertyConstants.EXPENSE_TYPE_META_CATEGORY_CODE, category.getCode()); fieldValues.put(TemPropertyConstants.CATEGORY_DEFAULT, Boolean.TRUE); fieldValues.put(KFSPropertyConstants.ACTIVE, Boolean.TRUE); Collection<ExpenseType> expenseTypes = getBusinessObjectService().findMatching(ExpenseType.class, fieldValues); if (expenseTypes == null || expenseTypes.isEmpty()) { return null; } Iterator<ExpenseType> expenseTypesIterator = expenseTypes.iterator(); ExpenseType returnValue = expenseTypesIterator.next(); while (expenseTypesIterator.hasNext()) { expenseTypesIterator.next(); // exhaust result set - which already *should* be exhausted but just in case } return returnValue; } /** * @see org.kuali.kfs.module.tem.service.TravelExpenseService#getExpenseServiceByType(org.kuali.kfs.module.tem.TemConstants.ExpenseType) */ @Override public TemExpenseService getExpenseServiceByType(TemConstants.ExpenseType expenseType){ return (TemExpenseService) SpringContext.getBean(TemExpense.class, expenseType.service); } /** * Gets the businessObjectService attribute. * @return Returns the businessObjectService. */ public BusinessObjectService getBusinessObjectService() { return businessObjectService; } /** * Sets the businessObjectService attribute value. * @param businessObjectService The businessObjectService to set. */ public void setBusinessObjectService(BusinessObjectService businessObjectService) { this.businessObjectService = businessObjectService; } /** * Gets the dateTimeService attribute. * @return Returns the dateTimeService. */ public DateTimeService getDateTimeService() { return dateTimeService; } /** * Sets the dateTimeService attribute value. * @param dateTimeService The dateTimeService to set. */ public void setDateTimeService(DateTimeService dateTimeService) { this.dateTimeService = dateTimeService; } private DocumentService getDocumentService() { return SpringContext.getBean(DocumentService.class); } public ExpenseTypeObjectCodeDao getExpenseTypeObjectCodeDao() { return expenseTypeObjectCodeDao; } public void setExpenseTypeObjectCodeDao(ExpenseTypeObjectCodeDao expenseTypeObjectCodeDao) { this.expenseTypeObjectCodeDao = expenseTypeObjectCodeDao; } public DocumentHelperService getDocumentHelperService() { return documentHelperService; } public void setDocumentHelperService(DocumentHelperService documentHelperService) { this.documentHelperService = documentHelperService; } public TravelService getTravelService() { return travelService; } public void setTravelService(TravelService travelService) { this.travelService = travelService; } /** * @see org.kuali.kfs.module.tem.service.TravelExpenseService#isTravelExpenseExceedReceiptRequirementThreshold(org.kuali.kfs.module.tem.businessobject.OtherExpense) */ @Override public boolean isTravelExpenseExceedReceiptRequirementThreshold(OtherExpense expense) { boolean isExceed = false; final ExpenseTypeObjectCode expenseTypeCode = expense.getExpenseTypeObjectCode(); if (expenseTypeCode.isReceiptRequired()) { //check for the threshold amount if (expenseTypeCode.getReceiptRequirementThreshold() != null){ KualiDecimal threshold = expenseTypeCode.getReceiptRequirementThreshold(); isExceed = threshold.isLessThan(expense.getExpenseAmount()); }else{ isExceed = true; } } return isExceed; } /** * @see org.kuali.kfs.module.tem.service.TravelExpenseService#isTripAccountingInformationEmpty(org.kuali.kfs.module.tem.businessobject.TripAccountingInformation) */ @Override public boolean isTripAccountingInformationEmpty(TripAccountingInformation accountingInformation) { return StringUtils.isEmpty(accountingInformation.getTripChartCode()) && StringUtils.isEmpty(accountingInformation.getTripAccountNumber()) && StringUtils.isEmpty(accountingInformation.getTripSubAccountNumber()) && StringUtils.isEmpty(accountingInformation.getObjectCode()) && StringUtils.isEmpty(accountingInformation.getSubObjectCode()) && StringUtils.isEmpty(accountingInformation.getProjectCode()) && StringUtils.isEmpty(accountingInformation.getOrganizationReference()); } /** * Copies the parent and nulls out unnecessary fields for a detail line * @see org.kuali.kfs.module.tem.service.TravelExpenseService#createNewDetailForActualExpense(org.kuali.kfs.module.tem.businessobject.ActualExpense) */ @Override public ActualExpense createNewDetailForActualExpense(ActualExpense parent) { ActualExpense newExpense = new ActualExpense(); try { BeanUtils.copyProperties(newExpense, parent); newExpense.setConvertedAmount(null); newExpense.setExpenseParentId(newExpense.getId()); newExpense.setId(null); newExpense.setNotes(null); newExpense.setExpenseLineTypeCode(null); // evidently, this should be nulled out on detail lines } catch (IllegalAccessException ex) { LOG.error(ex.getMessage(), ex); } catch (InvocationTargetException ex) { LOG.error(ex.getMessage(), ex); } return newExpense; } }