/*
* 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.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coa.service.ObjectCodeService;
import org.kuali.kfs.module.tem.TemConstants.ExpenseType;
import org.kuali.kfs.module.tem.businessobject.AccountingDistribution;
import org.kuali.kfs.module.tem.businessobject.TemDistributionAccountingLine;
import org.kuali.kfs.module.tem.businessobject.TemSourceAccountingLine;
import org.kuali.kfs.module.tem.businessobject.TravelerDetail;
import org.kuali.kfs.module.tem.businessobject.TripType;
import org.kuali.kfs.module.tem.document.TravelDocument;
import org.kuali.kfs.module.tem.document.TravelReimbursementDocument;
import org.kuali.kfs.module.tem.document.service.TravelDocumentService;
import org.kuali.kfs.module.tem.document.web.bean.AccountingLineDistributionKey;
import org.kuali.kfs.module.tem.service.AccountingDistributionService;
import org.kuali.kfs.module.tem.service.TravelExpenseService;
import org.kuali.kfs.module.tem.util.AccountingDistributionComparator;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.coreservice.framework.parameter.ParameterService;
import org.kuali.rice.krad.service.BusinessObjectService;
/**
* Accounting Distribution Service Implementation
*/
/**
* This class...
*/
public class AccountingDistributionServiceImpl implements AccountingDistributionService {
protected static Logger LOG = Logger.getLogger(AccountingDistributionServiceImpl.class);
protected BusinessObjectService businessObjectService;
protected ObjectCodeService objectCodeService;
protected TravelDocumentService travelDocumentService;
protected ParameterService parameterService;
@SuppressWarnings("deprecation")
@Override
public List<TemSourceAccountingLine> distributionToSouceAccountingLines(List<TemDistributionAccountingLine> distributionAccountingLines, List<AccountingDistribution> accountingDistributionList, KualiDecimal sourceAccountingLinesTotal, KualiDecimal expenseLimit){
List<TemSourceAccountingLine> sourceAccountingList = new ArrayList<TemSourceAccountingLine>();
Map<String, AccountingDistribution> distributionMap = new HashMap<String, AccountingDistribution>();
KualiDecimal total = KualiDecimal.ZERO;
int distributionTargetCount = 0;
boolean useExpenseLimit = false;
for (AccountingDistribution accountDistribution: accountingDistributionList){
if (accountDistribution.getSelected()){
total = total.add(accountDistribution.getRemainingAmount());
distributionTargetCount += 1;
}
}
if (expenseLimit != null && expenseLimit.isPositive()) {
KualiDecimal expenseLimitTotal = new KualiDecimal(expenseLimit.bigDecimalValue());
// do we have any accounting line amount to subtract from the expense limit?
if (sourceAccountingLinesTotal != null && sourceAccountingLinesTotal.isGreaterThan(KualiDecimal.ZERO)) {
expenseLimitTotal = expenseLimitTotal.subtract(sourceAccountingLinesTotal);
}
if (expenseLimitTotal.isLessThan(total)) {
total = expenseLimitTotal;
useExpenseLimit = true;
}
}
if (total.isGreaterThan(KualiDecimal.ZERO)) {
for (AccountingDistribution accountingDistribution : accountingDistributionList){
List<TemSourceAccountingLine> tempSourceAccountingList = new ArrayList<TemSourceAccountingLine>();
if (accountingDistribution.getSelected()){
for (TemDistributionAccountingLine accountingLine : distributionAccountingLines){
TemSourceAccountingLine newLine = new TemSourceAccountingLine();
try {
BeanUtils.copyProperties(newLine, accountingLine);
}
catch (IllegalAccessException ex) {
ex.printStackTrace();
}
catch (InvocationTargetException ex) {
ex.printStackTrace();
}
BigDecimal distributionAmount = (distributionTargetCount > 1) ? accountingDistribution.getRemainingAmount().bigDecimalValue() : total.bigDecimalValue();
BigDecimal product = accountingLine.getAccountLinePercent().multiply(distributionAmount);
product = product.divide(new BigDecimal(100),5,RoundingMode.HALF_UP);
BigDecimal lineAmount = product.divide(total.bigDecimalValue(),5,RoundingMode.HALF_UP);
newLine.setAmount(new KualiDecimal(product));
newLine.setCardType(accountingDistribution.getCardType());
Map<String,Object> fieldValues = new HashMap<String,Object>();
fieldValues.put(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, accountingDistribution.getObjectCode());
fieldValues.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, newLine.getChartOfAccountsCode());
fieldValues.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, SpringContext.getBean(UniversityDateService.class).getCurrentFiscalYear());
ObjectCode objCode = getBusinessObjectService().findByPrimaryKey(ObjectCode.class, fieldValues);
newLine.setObjectCode(objCode);
newLine.setFinancialObjectCode(accountingDistribution.getObjectCode());
tempSourceAccountingList.add(newLine);
}
if (useExpenseLimit) {
sourceAccountingList.addAll(tempSourceAccountingList); //we just adjusted the accounting lines for the expense...let's not readjust
} else {
sourceAccountingList.addAll(adjustValues(tempSourceAccountingList,accountingDistribution.getRemainingAmount()));
}
}
}
}
return sourceAccountingList;
}
/**
* This method will adjust the last accounting line to make the amounts sum up to the total.
*
* @param sourceAccountingList
* @param total
* @return
*/
protected List<TemSourceAccountingLine> adjustValues(List<TemSourceAccountingLine> sourceAccountingList, KualiDecimal total) {
KualiDecimal totalAmount = KualiDecimal.ZERO;
for (TemSourceAccountingLine newLine : sourceAccountingList) {
totalAmount = totalAmount.add(newLine.getAmount());
}
TemSourceAccountingLine line = sourceAccountingList.get(sourceAccountingList.size() - 1);
KualiDecimal remainderAmount = total.subtract(totalAmount);
if (remainderAmount.isPositive()) {
line.setAmount(line.getAmount().subtract(remainderAmount));
}
else if (remainderAmount.isNegative()) {
line.setAmount(line.getAmount().add(remainderAmount));
}
return sourceAccountingList;
}
/**
* @see org.kuali.kfs.module.tem.service.AccountingDistributionService#distributionToDistributionAccountingLine(java.util.List)
*/
@Override
public TemDistributionAccountingLine distributionToDistributionAccountingLine(List<AccountingDistribution> accountingDistributionList) {
KualiDecimal distributionTotal = KualiDecimal.ZERO;
for (AccountingDistribution accountingDistribution : accountingDistributionList) {
if (accountingDistribution.getSelected()) {
distributionTotal = distributionTotal.add(accountingDistribution.getRemainingAmount());
}
}
TemDistributionAccountingLine newLine = new TemDistributionAccountingLine();
newLine.setAmount(distributionTotal);
return newLine;
}
/**
* @see org.kuali.kfs.module.tem.service.AccountingDistributionService#createDistributions(org.kuali.kfs.module.tem.document.TravelDocument)
*/
@Override
public List<AccountingDistribution> createDistributions(TravelDocument travelDocument) {
List<AccountingDistribution> documentDistribution = new ArrayList<AccountingDistribution>();
Map<String, AccountingDistribution> distributionMap = new HashMap<String, AccountingDistribution>();
for (ExpenseType expense : EnumSet.allOf(ExpenseType.class)){
Map<String, AccountingDistribution> newDistributionMap = getTravelExpenseService().getExpenseServiceByType(expense).getAccountingDistribution(travelDocument);
addMergeDistributionMap(distributionMap, newDistributionMap);
}
subtractMergeDistributionMap(distributionMap, accountingLinesToDistributionMap(travelDocument));
for (String distribution : distributionMap.keySet()){
if (!distributionMap.get(distribution).getSubTotal().equals(KualiDecimal.ZERO)) { // don't include distributions of 0.00
documentDistribution.add(distributionMap.get(distribution));
}
}
Collections.sort(documentDistribution, new AccountingDistributionComparator());
return documentDistribution;
}
protected Map<String, AccountingDistribution> accountingLinesToDistributionMap(TravelDocument travelDocument) {
Map<String, AccountingDistribution> distributionMap = new HashMap<String, AccountingDistribution>();
for (TemSourceAccountingLine accountingLine : (List<TemSourceAccountingLine>) travelDocument.getSourceAccountingLines()) {
AccountingDistribution distribution = null;
String key = accountingLine.getObjectCode().getCode() + "-" + accountingLine.getCardType();
if (distributionMap.containsKey(key)) {
distributionMap.get(key).setSubTotal(distributionMap.get(key).getSubTotal().add(accountingLine.getAmount()));
}
else {
distribution = new AccountingDistribution();
distribution.setObjectCode(accountingLine.getObjectCode().getCode());
distribution.setSubTotal(accountingLine.getAmount());
distributionMap.put(key, distribution);
}
}
return distributionMap;
}
protected void addMergeDistributionMap(Map<String, AccountingDistribution> destinationMap, Map<String, AccountingDistribution> originMap) {
for (String key : originMap.keySet()) {
if (destinationMap.containsKey(key)) {
destinationMap.get(key).setSubTotal(destinationMap.get(key).getSubTotal().add(originMap.get(key).getSubTotal()));
destinationMap.get(key).setRemainingAmount(destinationMap.get(key).getRemainingAmount().add(originMap.get(key).getRemainingAmount()));
}
else {
destinationMap.put(key, originMap.get(key));
}
}
}
protected void subtractMergeDistributionMap(Map<String, AccountingDistribution> destinationMap, Map<String, AccountingDistribution> originMap) {
Iterator<String> it = originMap.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
if (destinationMap.containsKey(key)) {
destinationMap.get(key).setRemainingAmount(destinationMap.get(key).getRemainingAmount().subtract(originMap.get(key).getSubTotal()));
}
}
}
public String getObjectCodeFrom(final TravelDocument travelDocument, String paramName) {
final String parameterValue = getParameterService().getParameterValueAsString(TravelReimbursementDocument.class, paramName);
String paramSearchStr = "";
TravelerDetail traveler = travelDocument.getTraveler();
if(traveler != null){
paramSearchStr += traveler.getTravelerTypeCode() + "=";
}
TripType tripType = travelDocument.getTripType();
if(tripType != null){
paramSearchStr += tripType.getCode() + "=";
}
final int searchIdx = parameterValue.indexOf(paramSearchStr);
if (searchIdx == -1) {
return null;
}
final int endIdx = parameterValue.indexOf(";", searchIdx);
if (endIdx == -1) {
return parameterValue.substring(searchIdx + paramSearchStr.length());
}
return parameterValue.substring(searchIdx + paramSearchStr.length(), endIdx);
}
protected AccountingDistribution retrieveDistributionFor(final List<AccountingDistribution> distros, ObjectCode objectCode) {
if (objectCode != null) {
AccountingDistribution retval = retrieveDistributionFor(distros, objectCode.getCode());
retval.setObjectCode(objectCode.getCode());
retval.setObjectCodeName(objectCode.getName());
return retval;
}
return new AccountingDistribution();
}
protected AccountingDistribution retrieveDistributionFor(final List<AccountingDistribution> distros, final String code) {
if (distros != null && code != null) {
for (final AccountingDistribution distribution : distros) {
LOG.debug("comparing " + code + " to " + distribution.getObjectCode());
if (distribution.getObjectCode().equals(code)) {
return distribution;
}
}
}
return new AccountingDistribution();
}
/**
* @see org.kuali.kfs.module.tem.document.service.AccountingDistributionService#buildDistribution(TravelDocument)
*/
@Override
public List<AccountingDistribution> buildDistributionFrom(final TravelDocument travelDocument) {
List<AccountingDistribution> distributions = createDistributions(travelDocument);
return distributions;
}
@Override
public KualiDecimal getTotalAmount(List<TemDistributionAccountingLine> lines){
KualiDecimal total = KualiDecimal.ZERO;
for(TemDistributionAccountingLine line : lines){
total = total.add(line.getAmount());
}
return total;
}
@Override
public BigDecimal getTotalPercent(List<TemDistributionAccountingLine> lines){
BigDecimal total = new BigDecimal(0);
for(TemDistributionAccountingLine line : lines){
total = total.add(line.getAccountLinePercent());
}
return total;
}
/**
* @see org.kuali.kfs.module.tem.service.AccountingDistributionService#calculateAccountingLineDistributionPercent(java.util.List)
*/
@Override
public Map<AccountingLineDistributionKey, KualiDecimal> calculateAccountingLineDistributionPercent(List<SourceAccountingLine> accountingLine){
Map<AccountingLineDistributionKey, KualiDecimal> distributionMap = new HashMap<AccountingLineDistributionKey, KualiDecimal>();
//calculate the total from the accounting line
KualiDecimal total = KualiDecimal.ZERO;
for (SourceAccountingLine sourceLine : accountingLine){
total = total.add(sourceLine.getAmount());
}
//calculate the distribution on each of the accounting line distribution keys
AccountingLineDistributionKey key;
KualiDecimal factor;
for (SourceAccountingLine sourceLine : accountingLine){
key = new AccountingLineDistributionKey(sourceLine);
factor = sourceLine.getAmount().divide(total);
if (distributionMap.containsKey(key)){
//accumulate the stored percentage to the calculated; though this is probably very unlikely
factor = distributionMap.get(key).add(factor);
}
//store the final distribution to the map
distributionMap.put(key, factor);
}
return distributionMap;
}
/**
* Gets the parameterService attribute.
*
* @return Returns the parameterService.
*/
public ParameterService getParameterService() {
return parameterService;
}
/**
* Sets the parameterService attribute value.
*
* @param parameterService The parameterService to set.
*/
public void setParameterService(ParameterService parameterService) {
this.parameterService = parameterService;
}
public void setTravelDocumentService(final TravelDocumentService travelDocumentService) {
this.travelDocumentService = travelDocumentService;
}
protected TravelDocumentService getTravelDocumentService() {
return travelDocumentService;
}
/**
* Gets the objectCodeService attribute.
*
* @return Returns the objectCodeService.
*/
public ObjectCodeService getObjectCodeService() {
return objectCodeService;
}
/**
* Sets the objectCodeService attribute value.
*
* @param objectCodeService The objectCodeService to set.
*/
public void setObjectCodeService(ObjectCodeService objectCodeService) {
this.objectCodeService = objectCodeService;
}
public BusinessObjectService getBusinessObjectService() {
return businessObjectService;
}
public void setBusinessObjectService(BusinessObjectService businessObjectService) {
this.businessObjectService = businessObjectService;
}
public TravelExpenseService getTravelExpenseService(){
return SpringContext.getBean(TravelExpenseService.class);
}
}