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 }