/* * 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.cam.util; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.regex.Pattern; import org.apache.commons.beanutils.PropertyUtils; import org.kuali.kfs.module.cam.CamsConstants; import org.kuali.kfs.module.cam.businessobject.Asset; import org.kuali.kfs.module.cam.businessobject.AssetGlobal; import org.kuali.kfs.module.cam.businessobject.AssetGlobalDetail; import org.kuali.kfs.module.cam.businessobject.AssetPayment; import org.kuali.rice.core.api.util.type.KualiDecimal; /** * This class is a calculator which will distribute the amounts and balance them by ratio. Inputs received are * <li>Source Asset</li> * <li>Source Payments</li> * <li>Current max of payment number used by source Asset</li> * <li>AssetGlobal Document performing the separate action</li> * <li>List of new assets to be created for this separate request document</li> * Logic is best explained as below * <li>Compute the ratio of amounts to be removed from source payments</li> * <li>Compute the ratio by which each new asset should receive the allocated amount</li> * <li>Separate the allocate amount from the source payment using ratio computed above</li> * <li>Apply the allocate amount by ratio to each new asset</li> * <li>Adjust the last payment to round against the source from which split is done</li> * <li>Adjust the account charge amount of each asset by rounding the last payment with reference to user input separate amount</li> * <li>Create offset payments for the source asset</li> * <li>Compute accumulated depreciation amount for each payment, including offsets</li> */ public class AssetSeparatePaymentDistributor { private Asset sourceAsset; private AssetGlobal assetGlobal; private List<Asset> newAssets; private List<AssetPayment> sourcePayments = new ArrayList<AssetPayment>(); private List<AssetPayment> separatedPayments = new ArrayList<AssetPayment>(); private List<AssetPayment> offsetPayments = new ArrayList<AssetPayment>(); private List<AssetPayment> remainingPayments = new ArrayList<AssetPayment>(); private HashMap<Long, KualiDecimal> totalByAsset = new HashMap<Long, KualiDecimal>(); private HashMap<Integer, List<AssetPayment>> paymentSplitMap = new HashMap<Integer, List<AssetPayment>>(); private double[] assetAllocateRatios; private double separateRatio; private double retainRatio; private Integer maxPaymentSeqNo; private static PropertyDescriptor[] assetPaymentProperties = PropertyUtils.getPropertyDescriptors(AssetPayment.class); /** * Constructs a AssetSeparatePaymentDistributor.java. * * @param sourceAsset Source Asset * @param sourcePayments Source Payments * @param maxPaymentSeqNo Current max of payment number used by source Asset * @param assetGlobal AssetGlobal Document performing the separate action * @param newAssets List of new assets to be created for this separate request document */ public AssetSeparatePaymentDistributor(Asset sourceAsset, List<AssetPayment> sourcePayments, Integer maxPaymentSeqNo, AssetGlobal assetGlobal, List<Asset> newAssets) { super(); this.sourceAsset = sourceAsset; this.sourcePayments = sourcePayments; this.maxPaymentSeqNo = maxPaymentSeqNo; this.assetGlobal = assetGlobal; this.newAssets = newAssets; } public void distribute() { KualiDecimal totalSourceAmount = this.assetGlobal.getTotalCostAmount(); KualiDecimal totalSeparateAmount = this.assetGlobal.getSeparateSourceTotalAmount(); KualiDecimal remainingAmount = totalSourceAmount.subtract(totalSeparateAmount); // Compute separate ratio separateRatio = totalSeparateAmount.doubleValue() / totalSourceAmount.doubleValue(); // Compute the retained ratio retainRatio = remainingAmount.doubleValue() / totalSourceAmount.doubleValue(); List<AssetGlobalDetail> assetGlobalDetails = this.assetGlobal.getAssetGlobalDetails(); int size = assetGlobalDetails.size(); assetAllocateRatios = new double[size]; AssetGlobalDetail assetGlobalDetail = null; // Compute ratio by each asset for (int i = 0; i < size; i++) { assetGlobalDetail = assetGlobalDetails.get(i); Long capitalAssetNumber = assetGlobalDetail.getCapitalAssetNumber(); totalByAsset.put(capitalAssetNumber, KualiDecimal.ZERO); assetAllocateRatios[i] = assetGlobalDetail.getSeparateSourceAmount().doubleValue() / totalSeparateAmount.doubleValue(); } // Prepare the source and offset payments for split prepareSourcePaymentsForSplit(); // Distribute payments by ratio allocatePaymentAmountsByRatio(); // Round and balance by each payment line roundPaymentAmounts(); // Round and balance by separate source amount roundAccountChargeAmount(); // create offset payments createOffsetPayments(); } /** * Split the amount to be assigned from the source payments */ private void prepareSourcePaymentsForSplit() { // Call the allocate with ratio for each payments for (AssetPayment assetPayment : this.sourcePayments) { if (assetPayment.getAccountChargeAmount() != null && assetPayment.getAccountChargeAmount().isNonZero()) { // Separate amount AssetPayment separatePayment = new AssetPayment(); ObjectValueUtils.copySimpleProperties(assetPayment, separatePayment); this.separatedPayments.add(separatePayment); // Remaining amount AssetPayment remainingPayment = new AssetPayment(); ObjectValueUtils.copySimpleProperties(assetPayment, remainingPayment); this.remainingPayments.add(remainingPayment); applyRatioToPaymentAmounts(assetPayment, new AssetPayment[] { separatePayment, remainingPayment }, new double[] { separateRatio, retainRatio }); } } } /** * Creates offset payment by copying and negating the separated payments */ private void createOffsetPayments() { // create offset payment by negating the amount fields for (AssetPayment separatePayment : this.separatedPayments) { AssetPayment offsetPayment = new AssetPayment(); ObjectValueUtils.copySimpleProperties(separatePayment, offsetPayment); try { negatePaymentAmounts(offsetPayment); } catch (Exception e) { throw new RuntimeException(); } offsetPayment.setDocumentNumber(assetGlobal.getDocumentNumber()); offsetPayment.setFinancialDocumentTypeCode(CamsConstants.PaymentDocumentTypeCodes.ASSET_GLOBAL_SEPARATE); offsetPayment.setVersionNumber(null); offsetPayment.setObjectId(null); offsetPayment.setPaymentSequenceNumber(++maxPaymentSeqNo); this.offsetPayments.add(offsetPayment); } this.sourceAsset.getAssetPayments().addAll(this.offsetPayments); } /** * Applies the asset allocate ratio for each payment line to be created and adds to the new asset. In addition it keeps track of * how amount is consumed by each asset and how each payment is being split */ private void allocatePaymentAmountsByRatio() { int index = 0; for (AssetPayment source : this.separatedPayments) { // for each source payment, create target payments by ratio AssetPayment[] targets = new AssetPayment[assetAllocateRatios.length]; for (int j = 0; j < assetAllocateRatios.length; j++) { AssetPayment newPayment = new AssetPayment(); ObjectValueUtils.copySimpleProperties(source, newPayment); Asset currentAsset = this.newAssets.get(j); Long capitalAssetNumber = currentAsset.getCapitalAssetNumber(); newPayment.setCapitalAssetNumber(capitalAssetNumber); newPayment.setDocumentNumber(assetGlobal.getDocumentNumber()); newPayment.setFinancialDocumentTypeCode(CamsConstants.PaymentDocumentTypeCodes.ASSET_GLOBAL_SEPARATE); targets[j] = newPayment; newPayment.setVersionNumber(null); newPayment.setObjectId(null); currentAsset.getAssetPayments().add(index, newPayment); } applyRatioToPaymentAmounts(source, targets, assetAllocateRatios); // keep track of split happened for the source this.paymentSplitMap.put(source.getPaymentSequenceNumber(), Arrays.asList(targets)); // keep track of total amount by asset for (int j = 0; j < targets.length; j++) { Asset currentAsset = this.newAssets.get(j); Long capitalAssetNumber = currentAsset.getCapitalAssetNumber(); this.totalByAsset.put(capitalAssetNumber, this.totalByAsset.get(capitalAssetNumber).add(targets[j].getAccountChargeAmount())); } index++; } } /** * Rounds the last payment by adjusting the amounts against source amount */ private void roundPaymentAmounts() { for (int i = 0; i < this.separatedPayments.size(); i++) { applyBalanceToPaymentAmounts(separatedPayments.get(i), this.paymentSplitMap.get(separatedPayments.get(i).getPaymentSequenceNumber())); } } /** * Rounds the last payment by adjusting the amount compared against separate source amount and copies account charge amount to * primary depreciation base amount if not zero */ private void roundAccountChargeAmount() { for (int j = 0; j < this.newAssets.size(); j++) { Asset currentAsset = this.newAssets.get(j); AssetGlobalDetail detail = this.assetGlobal.getAssetGlobalDetails().get(j); AssetPayment lastPayment = currentAsset.getAssetPayments().get(currentAsset.getAssetPayments().size() - 1); KualiDecimal totalForAsset = this.totalByAsset.get(currentAsset.getCapitalAssetNumber()); KualiDecimal diff = detail.getSeparateSourceAmount().subtract(totalForAsset); lastPayment.setAccountChargeAmount(lastPayment.getAccountChargeAmount().add(diff)); currentAsset.setTotalCostAmount(totalForAsset.add(diff)); AssetPayment lastSource = this.separatedPayments.get(this.separatedPayments.size() - 1); lastSource.setAccountChargeAmount(lastSource.getAccountChargeAmount().add(diff)); // adjust primary depreciation base amount, same as account charge amount if (lastPayment.getPrimaryDepreciationBaseAmount() != null && lastPayment.getPrimaryDepreciationBaseAmount().isNonZero()) { lastPayment.setPrimaryDepreciationBaseAmount(lastPayment.getAccountChargeAmount()); lastSource.setPrimaryDepreciationBaseAmount(lastSource.getAccountChargeAmount()); } } } /** * Utility method which can take one payment and distribute its amount by ratio to the target payments * * @param source Source Payment * @param targets Target Payment * @param ratios Ratio to be applied for each target */ private void applyRatioToPaymentAmounts(AssetPayment source, AssetPayment[] targets, double[] ratios) { try { for (PropertyDescriptor propertyDescriptor : assetPaymentProperties) { Method readMethod = propertyDescriptor.getReadMethod(); if (readMethod != null && propertyDescriptor.getPropertyType() != null && KualiDecimal.class.isAssignableFrom(propertyDescriptor.getPropertyType())) { KualiDecimal amount = (KualiDecimal) readMethod.invoke(source); if (amount != null && amount.isNonZero()) { KualiDecimal[] ratioAmounts = KualiDecimalUtils.allocateByRatio(amount, ratios); Method writeMethod = propertyDescriptor.getWriteMethod(); if (writeMethod != null) { for (int i = 0; i < ratioAmounts.length; i++) { writeMethod.invoke(targets[i], ratioAmounts[i]); } } } } } } catch (Exception e) { throw new RuntimeException(e); } } /** * Utility method which can compute the difference between source amount and consumed amounts, then will adjust the last amount * * @param source Source payments * @param consumedList Consumed Payments */ private void applyBalanceToPaymentAmounts(AssetPayment source, List<AssetPayment> consumedList) { try { for (PropertyDescriptor propertyDescriptor : assetPaymentProperties) { Method readMethod = propertyDescriptor.getReadMethod(); if (readMethod != null && propertyDescriptor.getPropertyType() != null && KualiDecimal.class.isAssignableFrom(propertyDescriptor.getPropertyType())) { KualiDecimal amount = (KualiDecimal) readMethod.invoke(source); if (amount != null && amount.isNonZero()) { Method writeMethod = propertyDescriptor.getWriteMethod(); KualiDecimal consumedAmount = KualiDecimal.ZERO; KualiDecimal currAmt = KualiDecimal.ZERO; if (writeMethod != null) { for (int i = 0; i < consumedList.size(); i++) { currAmt = (KualiDecimal) readMethod.invoke(consumedList.get(i)); consumedAmount = consumedAmount.add(currAmt != null ? currAmt : KualiDecimal.ZERO); } } if (!consumedAmount.equals(amount)) { AssetPayment lastPayment = consumedList.get(consumedList.size() - 1); writeMethod.invoke(lastPayment, currAmt.add(amount.subtract(consumedAmount))); } } } } } catch (Exception e) { throw new RuntimeException(e); } } /** * Utility method which will negate the payment amounts for a given payment * * @param assetPayment Payment to be negated */ public void negatePaymentAmounts(AssetPayment assetPayment) { try { for (PropertyDescriptor propertyDescriptor : assetPaymentProperties) { Method readMethod = propertyDescriptor.getReadMethod(); if (readMethod != null && propertyDescriptor.getPropertyType() != null && KualiDecimal.class.isAssignableFrom(propertyDescriptor.getPropertyType())) { KualiDecimal amount = (KualiDecimal) readMethod.invoke(assetPayment); Method writeMethod = propertyDescriptor.getWriteMethod(); if (writeMethod != null && amount != null) { writeMethod.invoke(assetPayment, (amount.negated())); } } } } catch (Exception e) { throw new RuntimeException(e); } } /** * Sums up YTD values and Previous Year value to decide accumulated depreciation amount */ private void computeAccumulatedDepreciationAmount() { KualiDecimal previousYearAmount = null; for (Asset asset : this.newAssets) { List<AssetPayment> assetPayments = asset.getAssetPayments(); for (AssetPayment currPayment : assetPayments) { previousYearAmount = currPayment.getPreviousYearPrimaryDepreciationAmount(); previousYearAmount = previousYearAmount == null ? KualiDecimal.ZERO : previousYearAmount; KualiDecimal computedAmount = previousYearAmount.add(sumPeriodicDepreciationAmounts(currPayment)); if (computedAmount.isNonZero()) { currPayment.setAccumulatedPrimaryDepreciationAmount(computedAmount); } } } for (AssetPayment currPayment : this.offsetPayments) { previousYearAmount = currPayment.getPreviousYearPrimaryDepreciationAmount(); previousYearAmount = previousYearAmount == null ? KualiDecimal.ZERO : previousYearAmount; KualiDecimal computedAmount = previousYearAmount.add(sumPeriodicDepreciationAmounts(currPayment)); if (computedAmount.isNonZero()) { currPayment.setAccumulatedPrimaryDepreciationAmount(computedAmount); } } } /** * Sums up periodic amounts for a payment * * @param currPayment Payment * @return Sum of payment */ public static KualiDecimal sumPeriodicDepreciationAmounts(AssetPayment currPayment) { KualiDecimal ytdAmount = KualiDecimal.ZERO; try { for (PropertyDescriptor propertyDescriptor : assetPaymentProperties) { Method readMethod = propertyDescriptor.getReadMethod(); if (readMethod != null && Pattern.matches(CamsConstants.GET_PERIOD_DEPRECIATION_AMOUNT_REGEX, readMethod.getName().toLowerCase()) && propertyDescriptor.getPropertyType() != null && KualiDecimal.class.isAssignableFrom(propertyDescriptor.getPropertyType())) { KualiDecimal amount = (KualiDecimal) readMethod.invoke(currPayment); if (amount != null) { ytdAmount = ytdAmount.add(amount); } } } } catch (Exception e) { throw new RuntimeException(e); } return ytdAmount; } /** * Gets the remainingPayments attribute. * * @return Returns the remainingPayments. */ public List<AssetPayment> getRemainingPayments() { return remainingPayments; } /** * Sets the remainingPayments attribute value. * * @param remainingPayments The remainingPayments to set. */ public void setRemainingPayments(List<AssetPayment> remainingPayments) { this.remainingPayments = remainingPayments; } /** * Gets the offsetPayments attribute. * * @return Returns the offsetPayments. */ public List<AssetPayment> getOffsetPayments() { return offsetPayments; } /** * Sets the offsetPayments attribute value. * * @param offsetPayments The offsetPayments to set. */ public void setOffsetPayments(List<AssetPayment> offsetPayments) { this.offsetPayments = offsetPayments; } /** * Gets the separatedPayments attribute. * * @return Returns the separatedPayments. */ public List<AssetPayment> getSeparatedPayments() { return separatedPayments; } /** * Sets the separatedPayments attribute value. * * @param separatedPayments The separatedPayments to set. */ public void setSeparatedPayments(List<AssetPayment> separatedPayments) { this.separatedPayments = separatedPayments; } /** * Gets the assetGlobal attribute. * * @return Returns the assetGlobal. */ public AssetGlobal getAssetGlobal() { return assetGlobal; } /** * Sets the assetGlobal attribute value. * * @param assetGlobal The assetGlobal to set. */ public void setAssetGlobal(AssetGlobal assetGlobal) { this.assetGlobal = assetGlobal; } /** * Gets the newAssets attribute. * * @return Returns the newAssets. */ public List<Asset> getNewAssets() { return newAssets; } /** * Sets the newAssets attribute value. * * @param newAssets The newAssets to set. */ public void setNewAssets(List<Asset> newAssets) { this.newAssets = newAssets; } /** * Gets the assetAllocateRatios attribute. * * @return Returns the assetAllocateRatios. */ public double[] getAssetAllocateRatios() { return assetAllocateRatios; } /** * Sets the assetAllocateRatios attribute value. * * @param assetAllocateRatios The assetAllocateRatios to set. */ public void setAssetAllocateRatios(double[] assetAllocateRatios) { this.assetAllocateRatios = assetAllocateRatios; } /** * Gets the separateRatio attribute. * * @return Returns the separateRatio. */ public double getSeparateRatio() { return separateRatio; } /** * Sets the separateRatio attribute value. * * @param separateRatio The separateRatio to set. */ public void setSeparateRatio(double separateRatio) { this.separateRatio = separateRatio; } /** * Gets the retainRatio attribute. * * @return Returns the retainRatio. */ public double getRetainRatio() { return retainRatio; } /** * Sets the retainRatio attribute value. * * @param retainRatio The retainRatio to set. */ public void setRetainRatio(double retainRatio) { this.retainRatio = retainRatio; } /** * Gets the sourcePayments attribute. * * @return Returns the sourcePayments. */ public List<AssetPayment> getSourcePayments() { return sourcePayments; } /** * Sets the sourcePayments attribute value. * * @param sourcePayments The sourcePayments to set. */ public void setSourcePayments(List<AssetPayment> sourcePayments) { this.sourcePayments = sourcePayments; } }