001    /*
002     * Copyright 2011 The Kuali Foundation.
003     * 
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     * 
008     * http://www.opensource.org/licenses/ecl2.php
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.kfs.module.cam.util;
017    
018    import java.beans.PropertyDescriptor;
019    import java.lang.reflect.Method;
020    import java.util.ArrayList;
021    import java.util.Arrays;
022    import java.util.HashMap;
023    import java.util.List;
024    import java.util.regex.Pattern;
025    
026    import org.apache.commons.beanutils.PropertyUtils;
027    import org.kuali.kfs.module.cam.CamsConstants;
028    import org.kuali.kfs.module.cam.businessobject.Asset;
029    import org.kuali.kfs.module.cam.businessobject.AssetGlobal;
030    import org.kuali.kfs.module.cam.businessobject.AssetGlobalDetail;
031    import org.kuali.kfs.module.cam.businessobject.AssetPayment;
032    import org.kuali.rice.kns.util.KualiDecimal;
033    
034    /**
035     * This class is a calculator which will distribute the amounts and balance them by ratio. Inputs received are
036     * <li>Source Asset</li>
037     * <li>Source Payments</li>
038     * <li>Current max of payment number used by source Asset</li>
039     * <li>AssetGlobal Document performing the separate action</li>
040     * <li>List of new assets to be created for this separate request document</li>
041     * Logic is best explained as below
042     * <li>Compute the ratio of amounts to be removed from source payments</li>
043     * <li>Compute the ratio by which each new asset should receive the allocated amount</li>
044     * <li>Separate the allocate amount from the source payment using ratio computed above</li>
045     * <li>Apply the allocate amount by ratio to each new asset</li>
046     * <li>Adjust the last payment to round against the source from which split is done</li>
047     * <li>Adjust the account charge amount of each asset by rounding the last payment with reference to user input separate amount</li>
048     * <li>Create offset payments for the source asset</li>
049     * <li>Compute accumulated depreciation amount for each payment, including offsets</li>
050     */
051    public class AssetSeparatePaymentDistributor {
052        private Asset sourceAsset;
053        private AssetGlobal assetGlobal;
054        private List<Asset> newAssets;
055        private List<AssetPayment> sourcePayments = new ArrayList<AssetPayment>();
056        private List<AssetPayment> separatedPayments = new ArrayList<AssetPayment>();
057        private List<AssetPayment> offsetPayments = new ArrayList<AssetPayment>();
058        private List<AssetPayment> remainingPayments = new ArrayList<AssetPayment>();
059        private HashMap<Long, KualiDecimal> totalByAsset = new HashMap<Long, KualiDecimal>();
060        private HashMap<Integer, List<AssetPayment>> paymentSplitMap = new HashMap<Integer, List<AssetPayment>>();
061        private double[] assetAllocateRatios;
062        private double separateRatio;
063        private double retainRatio;
064        private Integer maxPaymentSeqNo;
065        private static PropertyDescriptor[] assetPaymentProperties = PropertyUtils.getPropertyDescriptors(AssetPayment.class);
066    
067    
068        /**
069         * Constructs a AssetSeparatePaymentDistributor.java.
070         * 
071         * @param sourceAsset Source Asset
072         * @param sourcePayments Source Payments
073         * @param maxPaymentSeqNo Current max of payment number used by source Asset
074         * @param assetGlobal AssetGlobal Document performing the separate action
075         * @param newAssets List of new assets to be created for this separate request document
076         */
077        public AssetSeparatePaymentDistributor(Asset sourceAsset, List<AssetPayment> sourcePayments, Integer maxPaymentSeqNo, AssetGlobal assetGlobal, List<Asset> newAssets) {
078            super();
079            this.sourceAsset = sourceAsset;
080            this.sourcePayments = sourcePayments;
081            this.maxPaymentSeqNo = maxPaymentSeqNo;
082            this.assetGlobal = assetGlobal;
083            this.newAssets = newAssets;
084        }
085    
086    
087        public void distribute() {
088            KualiDecimal totalSourceAmount = this.assetGlobal.getTotalCostAmount();
089            KualiDecimal totalSeparateAmount = this.assetGlobal.getSeparateSourceTotalAmount();
090            KualiDecimal remainingAmount = totalSourceAmount.subtract(totalSeparateAmount);
091            // Compute separate ratio
092            separateRatio = totalSeparateAmount.doubleValue() / totalSourceAmount.doubleValue();
093            // Compute the retained ratio
094            retainRatio = remainingAmount.doubleValue() / totalSourceAmount.doubleValue();
095            List<AssetGlobalDetail> assetGlobalDetails = this.assetGlobal.getAssetGlobalDetails();
096            int size = assetGlobalDetails.size();
097            assetAllocateRatios = new double[size];
098            AssetGlobalDetail assetGlobalDetail = null;
099            // Compute ratio by each asset
100            for (int i = 0; i < size; i++) {
101                assetGlobalDetail = assetGlobalDetails.get(i);
102                Long capitalAssetNumber = assetGlobalDetail.getCapitalAssetNumber();
103                totalByAsset.put(capitalAssetNumber, KualiDecimal.ZERO);
104                assetAllocateRatios[i] = assetGlobalDetail.getSeparateSourceAmount().doubleValue() / totalSeparateAmount.doubleValue();
105            }
106            // Prepare the source and offset payments for split
107            prepareSourcePaymentsForSplit();
108            // Distribute payments by ratio
109            allocatePaymentAmountsByRatio();
110            // Round and balance by each payment line
111            roundPaymentAmounts();
112            // Round and balance by separate source amount
113            roundAccountChargeAmount();
114            // create offset payments
115            createOffsetPayments();
116        }
117    
118    
119        /**
120         * Split the amount to be assigned from the source payments
121         */
122        private void prepareSourcePaymentsForSplit() {
123            // Call the allocate with ratio for each payments
124            for (AssetPayment assetPayment : this.sourcePayments) {
125                if (assetPayment.getAccountChargeAmount() != null && assetPayment.getAccountChargeAmount().isNonZero()) {
126                    // Separate amount
127                    AssetPayment separatePayment = new AssetPayment();
128                    ObjectValueUtils.copySimpleProperties(assetPayment, separatePayment);
129                    this.separatedPayments.add(separatePayment);
130    
131                    // Remaining amount
132                    AssetPayment remainingPayment = new AssetPayment();
133                    ObjectValueUtils.copySimpleProperties(assetPayment, remainingPayment);
134                    this.remainingPayments.add(remainingPayment);
135    
136                    applyRatioToPaymentAmounts(assetPayment, new AssetPayment[] { separatePayment, remainingPayment }, new double[] { separateRatio, retainRatio });
137                }
138            }
139    
140    
141        }
142    
143        /**
144         * Creates offset payment by copying and negating the separated payments
145         */
146        private void createOffsetPayments() {
147            // create offset payment by negating the amount fields
148            for (AssetPayment separatePayment : this.separatedPayments) {
149                AssetPayment offsetPayment = new AssetPayment();
150                ObjectValueUtils.copySimpleProperties(separatePayment, offsetPayment);
151                try {
152                    negatePaymentAmounts(offsetPayment);
153                }
154                catch (Exception e) {
155                    throw new RuntimeException();
156                }
157                offsetPayment.setDocumentNumber(assetGlobal.getDocumentNumber());
158                offsetPayment.setFinancialDocumentTypeCode(CamsConstants.PaymentDocumentTypeCodes.ASSET_GLOBAL_SEPARATE);
159                offsetPayment.setVersionNumber(null);
160                offsetPayment.setObjectId(null);
161                offsetPayment.setPaymentSequenceNumber(++maxPaymentSeqNo);
162                this.offsetPayments.add(offsetPayment);
163            }
164            this.sourceAsset.getAssetPayments().addAll(this.offsetPayments);
165        }
166    
167        /**
168         * Applies the asset allocate ratio for each payment line to be created and adds to the new asset. In addition it keeps track of
169         * how amount is consumed by each asset and how each payment is being split
170         */
171        private void allocatePaymentAmountsByRatio() {
172            int index = 0;
173            for (AssetPayment source : this.separatedPayments) {
174    
175                // for each source payment, create target payments by ratio
176                AssetPayment[] targets = new AssetPayment[assetAllocateRatios.length];
177                for (int j = 0; j < assetAllocateRatios.length; j++) {
178                    AssetPayment newPayment = new AssetPayment();
179                    ObjectValueUtils.copySimpleProperties(source, newPayment);
180                    Asset currentAsset = this.newAssets.get(j);
181                    Long capitalAssetNumber = currentAsset.getCapitalAssetNumber();
182                    newPayment.setCapitalAssetNumber(capitalAssetNumber);
183                    newPayment.setDocumentNumber(assetGlobal.getDocumentNumber());
184                    newPayment.setFinancialDocumentTypeCode(CamsConstants.PaymentDocumentTypeCodes.ASSET_GLOBAL_SEPARATE);
185                    targets[j] = newPayment;
186                    newPayment.setVersionNumber(null);
187                    newPayment.setObjectId(null);
188                    currentAsset.getAssetPayments().add(index, newPayment);
189                }
190                applyRatioToPaymentAmounts(source, targets, assetAllocateRatios);
191    
192                // keep track of split happened for the source
193                this.paymentSplitMap.put(source.getPaymentSequenceNumber(), Arrays.asList(targets));
194    
195                // keep track of total amount by asset
196                for (int j = 0; j < targets.length; j++) {
197                    Asset currentAsset = this.newAssets.get(j);
198                    Long capitalAssetNumber = currentAsset.getCapitalAssetNumber();
199                    this.totalByAsset.put(capitalAssetNumber, this.totalByAsset.get(capitalAssetNumber).add(targets[j].getAccountChargeAmount()));
200                }
201                index++;
202            }
203        }
204    
205        /**
206         * Rounds the last payment by adjusting the amounts against source amount
207         */
208        private void roundPaymentAmounts() {
209            for (int i = 0; i < this.separatedPayments.size(); i++) {
210                applyBalanceToPaymentAmounts(separatedPayments.get(i), this.paymentSplitMap.get(separatedPayments.get(i).getPaymentSequenceNumber()));
211            }
212        }
213    
214        /**
215         * Rounds the last payment by adjusting the amount compared against separate source amount and copies account charge amount to
216         * primary depreciation base amount if not zero
217         */
218        private void roundAccountChargeAmount() {
219            for (int j = 0; j < this.newAssets.size(); j++) {
220                Asset currentAsset = this.newAssets.get(j);
221                AssetGlobalDetail detail = this.assetGlobal.getAssetGlobalDetails().get(j);
222                AssetPayment lastPayment = currentAsset.getAssetPayments().get(currentAsset.getAssetPayments().size() - 1);
223                KualiDecimal totalForAsset = this.totalByAsset.get(currentAsset.getCapitalAssetNumber());
224                KualiDecimal diff = detail.getSeparateSourceAmount().subtract(totalForAsset);
225                lastPayment.setAccountChargeAmount(lastPayment.getAccountChargeAmount().add(diff));
226                currentAsset.setTotalCostAmount(totalForAsset.add(diff));
227                AssetPayment lastSource = this.separatedPayments.get(this.separatedPayments.size() - 1);
228                lastSource.setAccountChargeAmount(lastSource.getAccountChargeAmount().add(diff));
229                // adjust primary depreciation base amount, same as account charge amount
230                if (lastPayment.getPrimaryDepreciationBaseAmount() != null && lastPayment.getPrimaryDepreciationBaseAmount().isNonZero()) {
231                    lastPayment.setPrimaryDepreciationBaseAmount(lastPayment.getAccountChargeAmount());
232                    lastSource.setPrimaryDepreciationBaseAmount(lastSource.getAccountChargeAmount());
233                }
234            }
235        }
236    
237        /**
238         * Utility method which can take one payment and distribute its amount by ratio to the target payments
239         * 
240         * @param source Source Payment
241         * @param targets Target Payment
242         * @param ratios Ratio to be applied for each target
243         */
244        private void applyRatioToPaymentAmounts(AssetPayment source, AssetPayment[] targets, double[] ratios) {
245            try {
246                for (PropertyDescriptor propertyDescriptor : assetPaymentProperties) {
247                    Method readMethod = propertyDescriptor.getReadMethod();
248                    if (readMethod != null && propertyDescriptor.getPropertyType() != null && KualiDecimal.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
249                        KualiDecimal amount = (KualiDecimal) readMethod.invoke(source);
250                        if (amount != null && amount.isNonZero()) {
251                            KualiDecimal[] ratioAmounts = KualiDecimalUtils.allocateByRatio(amount, ratios);
252                            Method writeMethod = propertyDescriptor.getWriteMethod();
253                            if (writeMethod != null) {
254                                for (int i = 0; i < ratioAmounts.length; i++) {
255                                    writeMethod.invoke(targets[i], ratioAmounts[i]);
256                                }
257                            }
258                        }
259                    }
260                }
261            }
262            catch (Exception e) {
263                throw new RuntimeException(e);
264            }
265    
266        }
267    
268        /**
269         * Utility method which can compute the difference between source amount and consumed amounts, then will adjust the last amount
270         * 
271         * @param source Source payments
272         * @param consumedList Consumed Payments
273         */
274        private void applyBalanceToPaymentAmounts(AssetPayment source, List<AssetPayment> consumedList) {
275            try {
276                for (PropertyDescriptor propertyDescriptor : assetPaymentProperties) {
277                    Method readMethod = propertyDescriptor.getReadMethod();
278                    if (readMethod != null && propertyDescriptor.getPropertyType() != null && KualiDecimal.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
279                        KualiDecimal amount = (KualiDecimal) readMethod.invoke(source);
280                        if (amount != null && amount.isNonZero()) {
281                            Method writeMethod = propertyDescriptor.getWriteMethod();
282                            KualiDecimal consumedAmount = KualiDecimal.ZERO;
283                            KualiDecimal currAmt = KualiDecimal.ZERO;
284                            if (writeMethod != null) {
285                                for (int i = 0; i < consumedList.size(); i++) {
286                                    currAmt = (KualiDecimal) readMethod.invoke(consumedList.get(i));
287                                    consumedAmount = consumedAmount.add(currAmt != null ? currAmt : KualiDecimal.ZERO);
288                                }
289                            }
290                            if (!consumedAmount.equals(amount)) {
291                                AssetPayment lastPayment = consumedList.get(consumedList.size() - 1);
292                                writeMethod.invoke(lastPayment, currAmt.add(amount.subtract(consumedAmount)));
293                            }
294                        }
295                    }
296                }
297            }
298            catch (Exception e) {
299                throw new RuntimeException(e);
300            }
301    
302        }
303    
304        /**
305         * Utility method which will negate the payment amounts for a given payment
306         * 
307         * @param assetPayment Payment to be negated
308         */
309        public void negatePaymentAmounts(AssetPayment assetPayment) {
310            try {
311                for (PropertyDescriptor propertyDescriptor : assetPaymentProperties) {
312                    Method readMethod = propertyDescriptor.getReadMethod();
313                    if (readMethod != null && propertyDescriptor.getPropertyType() != null && KualiDecimal.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
314                        KualiDecimal amount = (KualiDecimal) readMethod.invoke(assetPayment);
315                        Method writeMethod = propertyDescriptor.getWriteMethod();
316                        if (writeMethod != null && amount != null) {
317                            writeMethod.invoke(assetPayment, (amount.negated()));
318                        }
319    
320                    }
321                }
322            }
323            catch (Exception e) {
324                throw new RuntimeException(e);
325            }
326        }
327    
328        /**
329         * Sums up YTD values and Previous Year value to decide accumulated depreciation amount
330         */
331        private void computeAccumulatedDepreciationAmount() {
332            KualiDecimal previousYearAmount = null;
333            for (Asset asset : this.newAssets) {
334                List<AssetPayment> assetPayments = asset.getAssetPayments();
335                for (AssetPayment currPayment : assetPayments) {
336                    previousYearAmount = currPayment.getPreviousYearPrimaryDepreciationAmount();
337                    previousYearAmount = previousYearAmount == null ? KualiDecimal.ZERO : previousYearAmount;
338                    KualiDecimal computedAmount = previousYearAmount.add(sumPeriodicDepreciationAmounts(currPayment));
339                    if (computedAmount.isNonZero()) {
340                        currPayment.setAccumulatedPrimaryDepreciationAmount(computedAmount);
341                    }
342                }
343            }
344            for (AssetPayment currPayment : this.offsetPayments) {
345                previousYearAmount = currPayment.getPreviousYearPrimaryDepreciationAmount();
346                previousYearAmount = previousYearAmount == null ? KualiDecimal.ZERO : previousYearAmount;
347                KualiDecimal computedAmount = previousYearAmount.add(sumPeriodicDepreciationAmounts(currPayment));
348                if (computedAmount.isNonZero()) {
349                    currPayment.setAccumulatedPrimaryDepreciationAmount(computedAmount);
350                }
351            }
352        }
353    
354        /**
355         * Sums up periodic amounts for a payment
356         * 
357         * @param currPayment Payment
358         * @return Sum of payment
359         */
360        public static KualiDecimal sumPeriodicDepreciationAmounts(AssetPayment currPayment) {
361            KualiDecimal ytdAmount = KualiDecimal.ZERO;
362            try {
363                for (PropertyDescriptor propertyDescriptor : assetPaymentProperties) {
364                    Method readMethod = propertyDescriptor.getReadMethod();
365                    if (readMethod != null && Pattern.matches(CamsConstants.GET_PERIOD_DEPRECIATION_AMOUNT_REGEX, readMethod.getName().toLowerCase()) && propertyDescriptor.getPropertyType() != null && KualiDecimal.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
366                        KualiDecimal amount = (KualiDecimal) readMethod.invoke(currPayment);
367                        if (amount != null) {
368                            ytdAmount = ytdAmount.add(amount);
369                        }
370                    }
371                }
372            }
373            catch (Exception e) {
374                throw new RuntimeException(e);
375            }
376            return ytdAmount;
377        }
378    
379        /**
380         * Gets the remainingPayments attribute.
381         * 
382         * @return Returns the remainingPayments.
383         */
384        public List<AssetPayment> getRemainingPayments() {
385            return remainingPayments;
386        }
387    
388    
389        /**
390         * Sets the remainingPayments attribute value.
391         * 
392         * @param remainingPayments The remainingPayments to set.
393         */
394        public void setRemainingPayments(List<AssetPayment> remainingPayments) {
395            this.remainingPayments = remainingPayments;
396        }
397    
398    
399        /**
400         * Gets the offsetPayments attribute.
401         * 
402         * @return Returns the offsetPayments.
403         */
404        public List<AssetPayment> getOffsetPayments() {
405            return offsetPayments;
406        }
407    
408    
409        /**
410         * Sets the offsetPayments attribute value.
411         * 
412         * @param offsetPayments The offsetPayments to set.
413         */
414        public void setOffsetPayments(List<AssetPayment> offsetPayments) {
415            this.offsetPayments = offsetPayments;
416        }
417    
418    
419        /**
420         * Gets the separatedPayments attribute.
421         * 
422         * @return Returns the separatedPayments.
423         */
424        public List<AssetPayment> getSeparatedPayments() {
425            return separatedPayments;
426        }
427    
428    
429        /**
430         * Sets the separatedPayments attribute value.
431         * 
432         * @param separatedPayments The separatedPayments to set.
433         */
434        public void setSeparatedPayments(List<AssetPayment> separatedPayments) {
435            this.separatedPayments = separatedPayments;
436        }
437    
438    
439        /**
440         * Gets the assetGlobal attribute.
441         * 
442         * @return Returns the assetGlobal.
443         */
444        public AssetGlobal getAssetGlobal() {
445            return assetGlobal;
446        }
447    
448    
449        /**
450         * Sets the assetGlobal attribute value.
451         * 
452         * @param assetGlobal The assetGlobal to set.
453         */
454        public void setAssetGlobal(AssetGlobal assetGlobal) {
455            this.assetGlobal = assetGlobal;
456        }
457    
458    
459        /**
460         * Gets the newAssets attribute.
461         * 
462         * @return Returns the newAssets.
463         */
464        public List<Asset> getNewAssets() {
465            return newAssets;
466        }
467    
468    
469        /**
470         * Sets the newAssets attribute value.
471         * 
472         * @param newAssets The newAssets to set.
473         */
474        public void setNewAssets(List<Asset> newAssets) {
475            this.newAssets = newAssets;
476        }
477    
478    
479        /**
480         * Gets the assetAllocateRatios attribute.
481         * 
482         * @return Returns the assetAllocateRatios.
483         */
484        public double[] getAssetAllocateRatios() {
485            return assetAllocateRatios;
486        }
487    
488    
489        /**
490         * Sets the assetAllocateRatios attribute value.
491         * 
492         * @param assetAllocateRatios The assetAllocateRatios to set.
493         */
494        public void setAssetAllocateRatios(double[] assetAllocateRatios) {
495            this.assetAllocateRatios = assetAllocateRatios;
496        }
497    
498    
499        /**
500         * Gets the separateRatio attribute.
501         * 
502         * @return Returns the separateRatio.
503         */
504        public double getSeparateRatio() {
505            return separateRatio;
506        }
507    
508    
509        /**
510         * Sets the separateRatio attribute value.
511         * 
512         * @param separateRatio The separateRatio to set.
513         */
514        public void setSeparateRatio(double separateRatio) {
515            this.separateRatio = separateRatio;
516        }
517    
518    
519        /**
520         * Gets the retainRatio attribute.
521         * 
522         * @return Returns the retainRatio.
523         */
524        public double getRetainRatio() {
525            return retainRatio;
526        }
527    
528    
529        /**
530         * Sets the retainRatio attribute value.
531         * 
532         * @param retainRatio The retainRatio to set.
533         */
534        public void setRetainRatio(double retainRatio) {
535            this.retainRatio = retainRatio;
536        }
537    
538    
539        /**
540         * Gets the sourcePayments attribute.
541         * 
542         * @return Returns the sourcePayments.
543         */
544        public List<AssetPayment> getSourcePayments() {
545            return sourcePayments;
546        }
547    
548    
549        /**
550         * Sets the sourcePayments attribute value.
551         * 
552         * @param sourcePayments The sourcePayments to set.
553         */
554        public void setSourcePayments(List<AssetPayment> sourcePayments) {
555            this.sourcePayments = sourcePayments;
556        }
557    
558    
559    }