/* * 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.sql.Date; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; import org.apache.log4j.Logger; import org.kuali.kfs.coa.businessobject.ObjectCode; import org.kuali.kfs.fp.document.DistributionOfIncomeAndExpenseDocument; import org.kuali.kfs.module.tem.TemConstants; import org.kuali.kfs.module.tem.TemParameterConstants; import org.kuali.kfs.module.tem.TemPropertyConstants; import org.kuali.kfs.module.tem.batch.service.ImportedExpensePendingEntryService; import org.kuali.kfs.module.tem.businessobject.AccountingDistribution; import org.kuali.kfs.module.tem.businessobject.ExpenseType; import org.kuali.kfs.module.tem.businessobject.ExpenseTypeObjectCode; import org.kuali.kfs.module.tem.businessobject.HistoricalExpenseAsTemExpenseWrapper; import org.kuali.kfs.module.tem.businessobject.HistoricalTravelExpense; import org.kuali.kfs.module.tem.businessobject.ImportedExpense; import org.kuali.kfs.module.tem.businessobject.TemExpense; import org.kuali.kfs.module.tem.businessobject.TemSourceAccountingLine; import org.kuali.kfs.module.tem.businessobject.TripAccountingInformation; import org.kuali.kfs.module.tem.document.TravelDocument; import org.kuali.kfs.module.tem.service.TemExpenseService; import org.kuali.kfs.module.tem.service.TravelExpenseService; import org.kuali.kfs.module.tem.util.ExpenseUtils; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry; import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper; import org.kuali.rice.core.api.util.type.KualiDecimal; import org.kuali.rice.krad.service.DataDictionaryService; import org.kuali.rice.krad.util.ObjectUtils; public class ImportedCTSExpenseServiceImpl extends ExpenseServiceBase implements TemExpenseService { private static Logger LOG = Logger.getLogger(ImportedCTSExpenseServiceImpl.class); protected ImportedExpensePendingEntryService importedExpensePendingEntryService; protected TravelExpenseService travelExpenseService; protected DataDictionaryService dataDictionaryService; /** * @see org.kuali.kfs.module.tem.service.TemExpenseService#calculateDistributionTotals(org.kuali.kfs.module.tem.document.TravelDocument, java.util.Map, java.util.List) */ @Override public void calculateDistributionTotals(TravelDocument document, Map<String, AccountingDistribution> distributionMap, List<? extends TemExpense> expenses){ String defaultChartCode = ExpenseUtils.getDefaultChartCode(document); for (TemExpense temExpense : expenses) { if (temExpense instanceof ImportedExpense) { ImportedExpense expense = (ImportedExpense)temExpense; String cardType = expense.getCardType(); if (cardType != null && cardType.equals(TemConstants.TRAVEL_TYPE_CTS) && !expense.getNonReimbursable()){ expense.refreshReferenceObject(TemPropertyConstants.EXPENSE_TYPE_OBJECT_CODE); String financialObjectCode= ""; expense.getExpenseTypeObjectCode(); if (expense.getExpenseTypeObjectCode().getExpenseTypeCode().equals(TemConstants.ExpenseTypes.AIRFARE)){ financialObjectCode= getParameterService().getParameterValueAsString(TemParameterConstants.TEM_ALL.class, TemConstants.AgencyMatchProcessParameter.TRAVEL_CREDIT_CARD_AIRFARE_OBJECT_CODE); } else if (expense.getExpenseTypeObjectCode().getExpenseTypeCode().equals(TemConstants.ExpenseTypes.LODGING)){ financialObjectCode= getParameterService().getParameterValueAsString(TemParameterConstants.TEM_ALL.class, TemConstants.AgencyMatchProcessParameter.TRAVEL_CREDIT_CARD_LODGING_OBJECT_CODE); } else if (expense.getExpenseTypeObjectCode().getExpenseTypeCode().equals(TemConstants.ExpenseTypes.RENTAL_CAR)){ financialObjectCode= getParameterService().getParameterValueAsString(TemParameterConstants.TEM_ALL.class, TemConstants.AgencyMatchProcessParameter.TRAVEL_CREDIT_CARD_RENTAL_CAR_OBJECT_CODE); } LOG.debug("Refreshed importedExpense with expense type code " + expense.getExpenseTypeObjectCode().getExpenseTypeCode() + " and financialObjectCode " + financialObjectCode); final ObjectCode objCode = getObjectCodeService().getByPrimaryIdForCurrentYear(defaultChartCode, expense.getExpenseTypeObjectCode().getFinancialObjectCode()); if (objCode != null && expense.getExpenseTypeObjectCode() != null && !expense.getExpenseTypeObjectCode().getExpenseType().isPrepaidExpense()){ AccountingDistribution distribution = null; String key = objCode.getCode() + "-" + TemConstants.TRAVEL_TYPE_CTS; if (distributionMap.containsKey(key)){ distributionMap.get(key).setSubTotal(distributionMap.get(key).getSubTotal().add(expense.getConvertedAmount())); distributionMap.get(key).setRemainingAmount(distributionMap.get(key).getRemainingAmount().add(expense.getConvertedAmount())); } else{ distribution = new AccountingDistribution(); distribution.setObjectCode(objCode.getCode()); distribution.setObjectCodeName(objCode.getName()); distribution.setCardType(cardType); distribution.setRemainingAmount(expense.getConvertedAmount()); distribution.setSubTotal(expense.getConvertedAmount()); distributionMap.put(key, distribution); } } } } } } /** * @see org.kuali.kfs.module.tem.service.impl.ExpenseServiceBase#getExpenseDetails(org.kuali.kfs.module.tem.document.TravelDocument) */ @Override public List<? extends TemExpense> getExpenseDetails(TravelDocument document) { final List<ImportedExpense> importedExpenses = document.getImportedExpenses(); Set<Long> importedHistoricalExpenseIds = new HashSet<Long>(); List<TemExpense> ctsExpenses = new ArrayList<TemExpense>(); for (ImportedExpense expense : importedExpenses) { if (StringUtils.equals(expense.getCardType(), TemConstants.TRAVEL_TYPE_CTS)) { ctsExpenses.add(expense); importedHistoricalExpenseIds.add(expense.getHistoricalTravelExpenseId()); } } // now include all HistoricalExpenses hung on the document final List<HistoricalTravelExpense> hungExpenses = document.getHistoricalTravelExpenses(); for (HistoricalTravelExpense expense : hungExpenses) { if (StringUtils.equals(expense.getCreditCardAgency().getTravelCardTypeCode(), TemConstants.TRAVEL_TYPE_CTS) && !importedHistoricalExpenseIds.contains(expense.getId())) { ctsExpenses.add(new HistoricalExpenseAsTemExpenseWrapper(expense)); } } return ctsExpenses; } /** * @see org.kuali.kfs.module.tem.service.TemExpenseService#validateExpenseCalculation(org.kuali.kfs.module.tem.businessobject.TemExpense) */ @Override public boolean validateExpenseCalculation(TemExpense expense){ return (expense instanceof ImportedExpense) && StringUtils.defaultString(((ImportedExpense)expense).getCardType()).equals(TemConstants.TRAVEL_TYPE_CTS) || (expense instanceof HistoricalExpenseAsTemExpenseWrapper && StringUtils.equals(((HistoricalExpenseAsTemExpenseWrapper)expense).getCardType(), TemConstants.TRAVEL_TYPE_CTS)); } /** * Used to create GLPE's for CTS imports. * * compares the entered accounting lines with what has been created in the trip account info table. * * if there are differences, create a credit glpe for the original account and a debit for the new one. * * If no change, original account info is correct and nothing needs to be done. */ @Override public void processExpense(TravelDocument travelDocument, GeneralLedgerPendingEntrySequenceHelper sequenceHelper){ String distributionIncomeAndExpenseDocumentType = getDataDictionaryService().getDocumentTypeNameByClass(DistributionOfIncomeAndExpenseDocument.class); //build map of the accounting line info and amount List<TemSourceAccountingLine> lines = travelDocument.getSourceAccountingLines(); Map<AccountingInfoKey,KualiDecimal> accountingLineMap = new HashMap<AccountingInfoKey, KualiDecimal>(); for (TemSourceAccountingLine line : lines){ if (line.getCardType().equals(TemConstants.TRAVEL_TYPE_CTS)){ AccountingInfoKey key = new AccountingInfoKey(line); KualiDecimal amount = line.getAmount(); if (accountingLineMap.containsKey(key)){ amount = accountingLineMap.get(key).add(line.getAmount()); } accountingLineMap.put(key, amount); } } //build map of expected amounts from CTS expenses Map<AccountingInfoKey,KualiDecimal> tripAccountMap = new HashMap<AccountingInfoKey, KualiDecimal>(); for (HistoricalTravelExpense historicalTravelExpense : travelDocument.getHistoricalTravelExpenses()){ //get historical travel expenses that are CTS for this document. if (travelDocument.getDocumentNumber().equals(historicalTravelExpense.getDocumentNumber()) && historicalTravelExpense.getAgencyStagingData() != null && historicalTravelExpense.getReconciled().equals(TemConstants.ReconciledCodes.UNRECONCILED)){ for (TripAccountingInformation tripAccountingInformation : historicalTravelExpense.getAgencyStagingData().getTripAccountingInformation()){ AccountingInfoKey key = new AccountingInfoKey(tripAccountingInformation, historicalTravelExpense); KualiDecimal amount = tripAccountingInformation.getAmount(); if (amount == null) { amount = historicalTravelExpense.getAmount().divide(new KualiDecimal(historicalTravelExpense.getAgencyStagingData().getTripAccountingInformation().size())); } if (tripAccountMap.containsKey(key)){ amount = tripAccountMap.get(key).add(amount); } tripAccountMap.put(key, amount); } } } /* * Iterate through imported expense accounts and match them to accounting line accounts. * process any changes by creating a new credit glpe */ for (AccountingInfoKey key : tripAccountMap.keySet()) { if (!accountingLineMap.containsKey(key) || accountingLineMap.get(key).isLessThan(tripAccountMap.get(key))){ //There is a difference in the accounts used //Either the account was completely switched, or was supplemented with another account. //create the credit glpe TemSourceAccountingLine creditLine = new TemSourceAccountingLine(); creditLine.setChartOfAccountsCode(key.getChartOfAccountsCode()); creditLine.setAccountNumber(key.getAccountNumber()); creditLine.setSubAccountNumber(key.getSubAccountNumber()); final ExpenseTypeObjectCode expenseTypeObjectCode = getExpenseTypeObjectCode(travelDocument, key.getHistoricalTravelExpense()); creditLine.setFinancialObjectCode(expenseTypeObjectCode.getFinancialObjectCode()); creditLine.setProjectCode(key.getProjectCode()); creditLine.setOrganizationReferenceId(key.getOrganizationReferenceId()); KualiDecimal amount = (accountingLineMap.get(key) == null?tripAccountMap.get(key):tripAccountMap.get(key).subtract(accountingLineMap.get(key))); creditLine.setAmount(amount); creditLine.setReferenceOriginCode(TemConstants.ORIGIN_CODE); importedExpensePendingEntryService.generateDocumentImportedExpenseGeneralLedgerPendingEntries(travelDocument, creditLine, sequenceHelper, true, distributionIncomeAndExpenseDocumentType); accountingLineMap.remove(key); } } /* * Iterate through the rest of the accounting lines. * Create normal debit glpe's. */ for (AccountingInfoKey key : accountingLineMap.keySet()){ TemSourceAccountingLine debitLine = new TemSourceAccountingLine(); debitLine.setChartOfAccountsCode(key.getChartOfAccountsCode()); debitLine.setAccountNumber(key.getAccountNumber()); debitLine.setSubAccountNumber(key.getSubAccountNumber()); debitLine.setFinancialObjectCode(key.getFinancialObjectCode()); debitLine.setFinancialSubObjectCode(key.getFinancialSubObjectCode()); debitLine.setProjectCode(key.getProjectCode()); debitLine.setOrganizationReferenceId(key.getOrganizationReferenceId()); debitLine.setAmount(accountingLineMap.get(key)); debitLine.setReferenceOriginCode(TemConstants.ORIGIN_CODE); importedExpensePendingEntryService.generateDocumentImportedExpenseGeneralLedgerPendingEntries(travelDocument, debitLine, sequenceHelper, false, distributionIncomeAndExpenseDocumentType); } // now, add the credit lines for each CTS for (ImportedExpense importedExpense : travelDocument.getImportedExpenses()) { if (StringUtils.equals(importedExpense.getCardType(), TemConstants.TRAVEL_TYPE_CTS)) { final List<GeneralLedgerPendingEntry> creditEntries = importedExpensePendingEntryService.buildDistributionEntriesForCTSExpense(importedExpense, sequenceHelper, travelDocument.getTravelDocumentIdentifier()); for (GeneralLedgerPendingEntry glpe : creditEntries) { travelDocument.addPendingEntry(glpe); } } } } /** * Looks up the expense type object for the given TravelDocument and HistoricalTravelExpense * @param document the document to find an expense type object code for * @param expense the historical travel expense, which has agency staging data that has an expense type * @return the expense type object code */ protected ExpenseTypeObjectCode getExpenseTypeObjectCode(TravelDocument document, HistoricalTravelExpense expense) { final TemConstants.ExpenseTypeMetaCategory expenseTypeCategory = expense.getAgencyStagingData().getExpenseTypeCategory(); final ExpenseType expenseType = getTravelExpenseService().getDefaultExpenseTypeForCategory(expenseTypeCategory); final String documentType = document.getDocumentTypeName(); final String tripType = StringUtils.isBlank(document.getTripTypeCode()) ? TemConstants.ALL_EXPENSE_TYPE_OBJECT_CODE_TRIP_TYPE : document.getTripTypeCode(); final String travelerType = ObjectUtils.isNull(document.getTraveler()) || StringUtils.isBlank(document.getTraveler().getTravelerTypeCode()) ? TemConstants.ALL_EXPENSE_TYPE_OBJECT_CODE_TRAVELER_TYPE : document.getTraveler().getTravelerTypeCode(); final ExpenseTypeObjectCode expenseTypeObjectCode = getTravelExpenseService().getExpenseType(expenseType.getCode(), documentType, tripType, travelerType); return expenseTypeObjectCode; } /** * Key which represents (and holds) fields of either a TripAccountingInformation or TemSourceAccountingLine, so that they can be matched against each other */ class AccountingInfoKey { protected String chartOfAccountsCode; protected String accountNumber; protected String subAccountNumber; protected String financialObjectCode; protected String financialSubObjectCode; protected String projectCode; protected String organizationReferenceId; protected HistoricalTravelExpense historicalTravelExpense; public AccountingInfoKey(TripAccountingInformation info, HistoricalTravelExpense expense) { chartOfAccountsCode = info.getTripChartCode(); accountNumber = info.getTripAccountNumber(); subAccountNumber = info.getTripSubAccountNumber(); projectCode = info.getProjectCode(); organizationReferenceId = info.getOrganizationReference(); historicalTravelExpense = expense; } public AccountingInfoKey(TemSourceAccountingLine accountingLine) { chartOfAccountsCode = accountingLine.getChartOfAccountsCode(); accountNumber = accountingLine.getAccountNumber(); subAccountNumber = accountingLine.getSubAccountNumber(); financialObjectCode = accountingLine.getFinancialObjectCode(); financialSubObjectCode = accountingLine.getFinancialSubObjectCode(); projectCode = accountingLine.getProjectCode(); organizationReferenceId = accountingLine.getOrganizationReferenceId(); } public String getChartOfAccountsCode() { return chartOfAccountsCode; } public String getAccountNumber() { return accountNumber; } public String getSubAccountNumber() { if (subAccountNumber == null) { return KFSConstants.EMPTY_STRING; } return subAccountNumber; } public String getFinancialObjectCode() { return financialObjectCode; } public String getFinancialSubObjectCode() { if (financialSubObjectCode == null) { return KFSConstants.EMPTY_STRING; } return financialSubObjectCode; } public String getProjectCode() { if (projectCode == null) { return KFSConstants.EMPTY_STRING; } return projectCode; } public String getOrganizationReferenceId() { if (organizationReferenceId == null) { return KFSConstants.EMPTY_STRING; } return organizationReferenceId; } public HistoricalTravelExpense getHistoricalTravelExpense() { return historicalTravelExpense; } @Override public boolean equals(Object o) { if (o == null || !(o instanceof AccountingInfoKey)) { return false; } AccountingInfoKey key = (AccountingInfoKey)o; EqualsBuilder equalsBuilder = new EqualsBuilder(); equalsBuilder.append(getChartOfAccountsCode(), key.getChartOfAccountsCode()); equalsBuilder.append(getAccountNumber(), key.getAccountNumber()); equalsBuilder.append(getSubAccountNumber(), key.getSubAccountNumber()); equalsBuilder.append(getProjectCode(), key.getProjectCode()); equalsBuilder.append(getOrganizationReferenceId(), key.getOrganizationReferenceId()); return equalsBuilder.isEquals(); } @Override public int hashCode() { HashCodeBuilder hashCodeBuilder = new HashCodeBuilder(); hashCodeBuilder.append(getChartOfAccountsCode()); hashCodeBuilder.append(getAccountNumber()); hashCodeBuilder.append(getSubAccountNumber()); hashCodeBuilder.append(getProjectCode()); hashCodeBuilder.append(getOrganizationReferenceId()); return hashCodeBuilder.toHashCode(); } } /** * @see org.kuali.kfs.module.tem.service.impl.ExpenseServiceBase#updateExpense(org.kuali.kfs.module.tem.document.TravelDocument) */ @Override public void updateExpense(TravelDocument travelDocument) { List<HistoricalTravelExpense> historicalTravelExpenses = travelDocument.getHistoricalTravelExpenses(); for (HistoricalTravelExpense historicalTravelExpense : historicalTravelExpenses){ if (historicalTravelExpense.getAgencyStagingDataId() != null && (StringUtils.isBlank(historicalTravelExpense.getReconciled()) || StringUtils.equals(historicalTravelExpense.getReconciled(), TemConstants.ReconciledCodes.UNRECONCILED))) { long time = (new java.util.Date()).getTime(); historicalTravelExpense.setReconciliationDate(new Date(time)); historicalTravelExpense.setReconciled(TemConstants.ReconciledCodes.RECONCILED); } } getBusinessObjectService().save(historicalTravelExpenses); } public void setImportedExpensePendingEntryService(ImportedExpensePendingEntryService importedExpensePendingEntryService) { this.importedExpensePendingEntryService = importedExpensePendingEntryService; } public TravelExpenseService getTravelExpenseService() { return travelExpenseService; } public void setTravelExpenseService(TravelExpenseService travelExpenseService) { this.travelExpenseService = travelExpenseService; } public DataDictionaryService getDataDictionaryService() { return dataDictionaryService; } public void setDataDictionaryService(DataDictionaryService dataDictionaryService) { this.dataDictionaryService = dataDictionaryService; } }