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.ar.document.validation.impl;
017
018 import java.text.MessageFormat;
019 import java.util.HashMap;
020 import java.util.List;
021 import java.util.Map;
022
023 import org.apache.commons.lang.StringUtils;
024 import org.kuali.kfs.coa.businessobject.Account;
025 import org.kuali.kfs.coa.businessobject.Chart;
026 import org.kuali.kfs.coa.businessobject.ObjectCode;
027 import org.kuali.kfs.coa.businessobject.ProjectCode;
028 import org.kuali.kfs.coa.businessobject.SubAccount;
029 import org.kuali.kfs.coa.businessobject.SubObjectCode;
030 import org.kuali.kfs.module.ar.ArKeyConstants;
031 import org.kuali.kfs.module.ar.ArPropertyConstants;
032 import org.kuali.kfs.module.ar.businessobject.CustomerInvoiceDetail;
033 import org.kuali.kfs.module.ar.businessobject.InvoicePaidApplied;
034 import org.kuali.kfs.module.ar.businessobject.NonAppliedHolding;
035 import org.kuali.kfs.module.ar.businessobject.NonInvoiced;
036 import org.kuali.kfs.module.ar.document.CashControlDocument;
037 import org.kuali.kfs.module.ar.document.PaymentApplicationDocument;
038 import org.kuali.kfs.sys.context.SpringContext;
039 import org.kuali.rice.kew.exception.WorkflowException;
040 import org.kuali.rice.kns.service.BusinessObjectService;
041 import org.kuali.rice.kns.service.DictionaryValidationService;
042 import org.kuali.rice.kns.util.GlobalVariables;
043 import org.kuali.rice.kns.util.KualiDecimal;
044 import org.kuali.rice.kns.util.MessageMap;
045 import org.kuali.rice.kns.util.ObjectUtils;
046
047 public class PaymentApplicationDocumentRuleUtil {
048
049 public static boolean validateAllAmounts(PaymentApplicationDocument applicationDocument, List<CustomerInvoiceDetail> invoiceDetails, NonInvoiced newNonInvoiced, KualiDecimal totalFromControl) throws WorkflowException {
050 boolean isValid = validateApplieds(invoiceDetails, applicationDocument, totalFromControl);
051 isValid &= validateNonAppliedHolding(applicationDocument, totalFromControl);
052 isValid &= validateNonInvoiced(newNonInvoiced, applicationDocument, totalFromControl);
053 return isValid;
054 }
055
056 /**
057 * This method checks that an invoice paid applied is for a valid amount.
058 *
059 * @param invoicePaidApplied
060 * @return
061 */
062 public static boolean validateInvoicePaidApplied(InvoicePaidApplied invoicePaidApplied, String fieldName, KualiDecimal totalFromControl) {
063 boolean isValid = true;
064
065 invoicePaidApplied.refreshReferenceObject("invoiceDetail");
066 if(ObjectUtils.isNull(invoicePaidApplied) || ObjectUtils.isNull(invoicePaidApplied.getInvoiceDetail())) { return true; }
067 KualiDecimal amountOwed = invoicePaidApplied.getInvoiceDetail().getAmountOpen();
068 KualiDecimal amountPaid = invoicePaidApplied.getInvoiceItemAppliedAmount();
069
070 if(ObjectUtils.isNull(amountOwed)) {
071 amountOwed = KualiDecimal.ZERO;
072 }
073 if(ObjectUtils.isNull(amountPaid)) {
074 amountPaid = KualiDecimal.ZERO;
075 }
076
077 // Can't pay more than you owe.
078 if(!amountPaid.isLessEqual(amountOwed)) {
079 isValid = false;
080 GlobalVariables.getMessageMap().putError(
081 fieldName, ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_EXCEEDS_AMOUNT_OUTSTANDING);
082 }
083
084 // Can't apply more than the amount received via the related CashControlDocument
085 if (amountPaid.isGreaterThan(totalFromControl)) {
086 isValid = false;
087 GlobalVariables.getMessageMap().putError(
088 fieldName,ArKeyConstants.PaymentApplicationDocumentErrors.CANNOT_APPLY_MORE_THAN_CASH_CONTROL_TOTAL_AMOUNT);
089 }
090
091 // cant apply negative amounts
092 if (amountPaid.isNegative()) {
093 isValid = false;
094 GlobalVariables.getMessageMap().putError(
095 fieldName,ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_MUST_BE_POSTIIVE);
096 }
097 return isValid;
098 }
099
100 /**
101 * The sum of invoice paid applied amounts cannot exceed the cash control total amount
102 *
103 * @param paymentApplicationDocument
104 * @return
105 * @throws WorkflowException
106 */
107 public static boolean validateCumulativeSumOfInvoicePaidAppliedDoesntExceedCashControlTotal(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException {
108 KualiDecimal appliedTotal = new KualiDecimal(0);
109 for(InvoicePaidApplied invoicePaidApplied : paymentApplicationDocument.getInvoicePaidApplieds()) {
110 invoicePaidApplied.refreshReferenceObject("invoiceDetail");
111 appliedTotal = appliedTotal.add(invoicePaidApplied.getInvoiceItemAppliedAmount());
112 }
113 return paymentApplicationDocument.getTotalFromControl().isGreaterEqual(appliedTotal);
114 }
115
116 /**
117 * The sum of invoice paid applied amounts cannot be less than zero.
118 *
119 * @param paymentApplicationDocument
120 * @return
121 * @throws WorkflowException
122 */
123 public static boolean validateCumulativeSumOfInvoicePaidAppliedsIsGreaterThanOrEqualToZero(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException {
124 KualiDecimal appliedTotal = new KualiDecimal(0);
125 for(InvoicePaidApplied invoicePaidApplied : paymentApplicationDocument.getInvoicePaidApplieds()) {
126 invoicePaidApplied.refreshReferenceObject("invoiceDetail");
127 appliedTotal = appliedTotal.add(invoicePaidApplied.getInvoiceItemAppliedAmount());
128 }
129 return KualiDecimal.ZERO.isLessEqual(appliedTotal);
130 }
131
132 /**
133 * The sum of non invoiceds must be less than or equal to the cash control total amount
134 *
135 * @param paymentApplicationDocument
136 * @return
137 * @throws WorkflowException
138 */
139 public static boolean validateNonInvoicedAmountDoesntExceedCashControlTotal(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException {
140 return paymentApplicationDocument.getTotalFromControl().isGreaterEqual(paymentApplicationDocument.getSumOfNonInvoiceds());
141 }
142
143 /**
144 * The unapplied amount can't be negative
145 *
146 * @param paymentApplicationDocument
147 * @return
148 * @throws WorkflowException
149 */
150 public static boolean validateNonInvoicedAmountIsGreaterThanOrEqualToZero(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException {
151 return KualiDecimal.ZERO.isLessEqual(paymentApplicationDocument.getSumOfNonInvoiceds());
152 }
153
154 /**
155 * The unapplied amount must be less than or equal to the cash control total amount
156 *
157 * @param paymentApplicationDocument
158 * @return
159 * @throws WorkflowException
160 */
161 public static boolean validateUnappliedAmountDoesntExceedCashControlTotal(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException {
162 KualiDecimal a = paymentApplicationDocument.getNonAppliedHoldingAmount();
163 if(null == a) {
164 return true;
165 }
166 return paymentApplicationDocument.getTotalFromControl().isGreaterEqual(a);
167 }
168
169 /**
170 * The unapplied amount can't be negative
171 *
172 * @param paymentApplicationDocument
173 * @return
174 * @throws WorkflowException
175 */
176 public static boolean validateUnappliedAmountIsGreaterThanOrEqualToZero(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException {
177 if(null == paymentApplicationDocument.getNonAppliedHoldingAmount()) { return true; }
178 return KualiDecimal.ZERO.isLessEqual(paymentApplicationDocument.getNonAppliedHoldingAmount());
179 }
180
181 /**
182 * Validate non-ar/non-invoice line items on a PaymentApplicationDocument.
183 *
184 * @param nonInvoiced
185 * @return
186 */
187 public static boolean validateNonInvoiced(NonInvoiced nonInvoiced, PaymentApplicationDocument paymentApplicationDocument, KualiDecimal totalFromControl) throws WorkflowException {
188 MessageMap errorMap = GlobalVariables.getMessageMap();
189 int originalErrorCount = errorMap.getErrorCount();
190
191 // validate the NonInvoiced BO
192 String sNonInvoicedErrorPath = "nonInvoicedAddLine";
193 errorMap.addToErrorPath(sNonInvoicedErrorPath);
194 SpringContext.getBean(DictionaryValidationService.class).validateBusinessObject(nonInvoiced);
195 errorMap.removeFromErrorPath(sNonInvoicedErrorPath);
196
197 if (errorMap.getErrorCount() != originalErrorCount) {
198 return false;
199 }
200
201 boolean isValid = true;
202
203 // Required fields, so always validate these.
204 isValid &= validateNonInvoicedLineItem("chartOfAccountsCode", nonInvoiced.getChartOfAccountsCode(), Chart.class,
205 ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_CHART,
206 ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_CHART_INVALID);
207 isValid &= validateNonInvoicedLineItem("accountNumber", nonInvoiced.getAccountNumber(), Account.class,
208 ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_ACCOUNT,
209 ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_ACCOUNT_INVALID);
210 isValid &= validateNonInvoicedLineItem("financialObjectCode", nonInvoiced.getFinancialObjectCode(), ObjectCode.class,
211 ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_OBJECT,
212 ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_OBJECT_CODE_INVALID);
213
214 // Optional fields, so only validate if a value was entered.
215 if(StringUtils.isNotBlank(nonInvoiced.getSubAccountNumber())) {
216 isValid &= validateNonInvoicedLineItem("subAccountNumber", nonInvoiced.getSubAccountNumber(), SubAccount.class,
217 ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_SUBACCOUNT,
218 ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_SUB_ACCOUNT_INVALID);
219 }
220 if(StringUtils.isNotBlank(nonInvoiced.getFinancialSubObjectCode())) {
221 isValid &= validateNonInvoicedLineItem("financialSubObjectCode", nonInvoiced.getFinancialSubObjectCode(), SubObjectCode.class,
222 ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_SUBOBJECT,
223 ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_SUB_OBJECT_CODE_INVALID);
224 }
225 if(StringUtils.isNotBlank(nonInvoiced.getProjectCode())) {
226 isValid &= validateNonInvoicedLineItem("code", nonInvoiced.getProjectCode(), ProjectCode.class,
227 ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_PROJECT,
228 ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_PROJECT_CODE_INVALID);
229 }
230
231 isValid &= validateNonInvoicedLineAmount(nonInvoiced, paymentApplicationDocument, totalFromControl);
232
233 return isValid;
234 }
235
236 /**
237 * This method validates the provided non invoiced line value.
238 *
239 * @param attributeName The name of the attribute as it is defined within its parent business object (ie. financialObjectCode in ObjectCode.java)
240 * @param value The value of the NonInvoiced line to be validated.
241 * @param boClass The class that the provided value represents (ie. accountNumber represents Account.class)
242 * @param errorPropertyName The Payment Application document property name to be used for applying errors when necessary.
243 * @param errorMessageKey The error key path to be used for applying errors when necessary.
244 * @return True if the value provided is valid and exists, false otherwise.
245 */
246 private static boolean validateNonInvoicedLineItem(String attributeName, String value, Class boClass, String errorPropertyName, String errorMessageKey) {
247 MessageMap errorMap = GlobalVariables.getMessageMap();
248 boolean isValid = true;
249 Map<String, String> criteria = new HashMap<String, String>();
250 criteria.put(attributeName, value);
251
252 Object object = SpringContext.getBean(BusinessObjectService.class).findByPrimaryKey(boClass, criteria);
253 if(ObjectUtils.isNull(object)) {
254 errorMap.putError(errorPropertyName, errorMessageKey, value);
255 isValid &= false;
256 }
257 return isValid;
258 }
259
260 /**
261 * This method...
262 * @param nonInvoiced
263 * @param paymentApplicationDocument
264 * @param totalFromControl
265 * @return
266 */
267 private static boolean validateNonInvoicedLineAmount(NonInvoiced nonInvoiced, PaymentApplicationDocument paymentApplicationDocument, KualiDecimal totalFromControl) {
268 MessageMap errorMap = GlobalVariables.getMessageMap();
269 KualiDecimal nonArLineAmount = nonInvoiced.getFinancialDocumentLineAmount();
270 // check that dollar amount is not zero before continuing
271 if(ObjectUtils.isNull(nonArLineAmount)) {
272 errorMap.putError(
273 ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_AMOUNT,
274 ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_AMOUNT_REQUIRED);
275 return false;
276 } else {
277 KualiDecimal cashControlBalanceToBeApplied = totalFromControl;
278 cashControlBalanceToBeApplied = cashControlBalanceToBeApplied.add(paymentApplicationDocument.getTotalFromControl());
279 cashControlBalanceToBeApplied.subtract(paymentApplicationDocument.getTotalApplied());
280 cashControlBalanceToBeApplied.subtract(paymentApplicationDocument.getNonAppliedHoldingAmount());
281
282 if (nonArLineAmount.isZero()) {
283 errorMap.putError(
284 ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_AMOUNT,
285 ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_CANNOT_BE_ZERO);
286 return false;
287 }
288 else if (nonArLineAmount.isNegative()) {
289 errorMap.putError(
290 ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_AMOUNT,
291 ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_MUST_BE_POSTIIVE);
292 return false;
293 }
294 // check that we're not trying to apply more funds to the invoice than the invoice has balance (ie, over-applying)
295 else if (KualiDecimal.ZERO.isGreaterThan(cashControlBalanceToBeApplied.subtract(nonArLineAmount))) {
296 errorMap.putError(
297 ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_AMOUNT,
298 ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_AMOUNT_EXCEEDS_BALANCE_TO_BE_APPLIED);
299 return false;
300 }
301
302 }
303 return true;
304 }
305
306 /**
307 * This method determines whether or not the amount to be applied to an invoice is acceptable.
308 *
309 * @param customerInvoiceDetails
310 * @return
311 */
312 public static boolean validateApplieds(List<CustomerInvoiceDetail> customerInvoiceDetails, PaymentApplicationDocument paymentAplicationDocument, KualiDecimal totalFromControl) throws WorkflowException {
313
314 // Indicates whether the validation succeeded
315 boolean isValid = true;
316
317 // Figure out the maximum we should be able to apply.
318 for (CustomerInvoiceDetail customerInvoiceDetail : customerInvoiceDetails) {
319 isValid &= validateAmountAppliedToCustomerInvoiceDetailByPaymentApplicationDocument(customerInvoiceDetail, paymentAplicationDocument, totalFromControl);
320 }
321
322 return isValid;
323 }
324
325 /**
326 * @param customerInvoiceDetail
327 * @param paymentApplicationDocument
328 * @return
329 */
330 public static boolean validateAmountAppliedToCustomerInvoiceDetailByPaymentApplicationDocument(CustomerInvoiceDetail customerInvoiceDetail, PaymentApplicationDocument paymentApplicationDocument, KualiDecimal totalFromControl) throws WorkflowException {
331
332 boolean isValid = true;
333
334 // This let's us highlight a specific invoice detail line
335 String propertyName =
336 MessageFormat.format(ArPropertyConstants.PaymentApplicationDocumentFields.AMOUNT_TO_BE_APPLIED_LINE_N, customerInvoiceDetail.getSequenceNumber().toString());
337
338 KualiDecimal amountAppliedByAllOtherDocuments =
339 customerInvoiceDetail.getAmountAppliedExcludingAnyAmountAppliedBy(paymentApplicationDocument.getDocumentNumber());
340 KualiDecimal amountAppliedByThisDocument =
341 customerInvoiceDetail.getAmountAppliedBy(paymentApplicationDocument.getDocumentNumber());
342 KualiDecimal totalAppliedAmount =
343 amountAppliedByAllOtherDocuments.add(amountAppliedByThisDocument);
344
345 // Can't apply more than the total amount of the detail
346 if(!totalAppliedAmount.isLessEqual(totalFromControl)) {
347 isValid = false;
348 GlobalVariables.getMessageMap().putError(propertyName, ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_EXCEEDS_AMOUNT_OUTSTANDING);
349 }
350
351 // Can't apply a negative amount.
352 if(KualiDecimal.ZERO.isGreaterThan(amountAppliedByThisDocument)) {
353 isValid = false;
354 GlobalVariables.getMessageMap().putError(propertyName, ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_MUST_BE_GREATER_THAN_ZERO);
355 }
356
357 // Can't apply more than the total amount outstanding on the cash control document.
358 CashControlDocument cashControlDocument = paymentApplicationDocument.getCashControlDocument();
359 if(ObjectUtils.isNotNull(cashControlDocument)) {
360 if(cashControlDocument.getCashControlTotalAmount().isLessThan(amountAppliedByThisDocument)) {
361 isValid = false;
362 GlobalVariables.getMessageMap().putError(propertyName, ArKeyConstants.PaymentApplicationDocumentErrors.CANNOT_APPLY_MORE_THAN_BALANCE_TO_BE_APPLIED);
363 }
364 }
365
366 return isValid;
367 }
368
369 /**
370 * This method validates the unapplied attribute of the document.
371 *
372 * @param document
373 * @return
374 * @throws WorkflowException
375 */
376 public static boolean validateNonAppliedHolding(PaymentApplicationDocument applicationDocument, KualiDecimal totalFromControl) throws WorkflowException {
377 NonAppliedHolding nonAppliedHolding = applicationDocument.getNonAppliedHolding();
378 if(ObjectUtils.isNull(nonAppliedHolding)) { return true; }
379 if(StringUtils.isNotEmpty(nonAppliedHolding.getCustomerNumber())) {
380 KualiDecimal nonAppliedAmount = nonAppliedHolding.getFinancialDocumentLineAmount();
381 if(null == nonAppliedAmount) { nonAppliedAmount = KualiDecimal.ZERO; }
382 boolean isValid = totalFromControl.isGreaterEqual(nonAppliedAmount);
383 if(!isValid) {
384 String propertyName = ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_AMOUNT;
385 String errorKey = ArKeyConstants.PaymentApplicationDocumentErrors.UNAPPLIED_AMOUNT_CANNOT_EXCEED_AVAILABLE_AMOUNT;
386 GlobalVariables.getMessageMap().putError(propertyName, errorKey);
387 }
388 // The amount of the unapplied can't exceed the remaining balance to be applied
389 KualiDecimal totalBalanceToBeApplied = applicationDocument.getUnallocatedBalance();
390 isValid = KualiDecimal.ZERO.isLessEqual(totalBalanceToBeApplied);
391 if(!isValid) {
392 String propertyName = ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_AMOUNT;
393 String errorKey = ArKeyConstants.PaymentApplicationDocumentErrors.UNAPPLIED_AMOUNT_CANNOT_EXCEED_BALANCE_TO_BE_APPLIED;
394 GlobalVariables.getMessageMap().putError(propertyName, errorKey);
395 }
396
397 // the unapplied amount cannot be negative
398 isValid = nonAppliedAmount.isPositive() || nonAppliedAmount.isZero();
399 if (!isValid) {
400 String propertyName = ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_AMOUNT;
401 String errorKey = ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_MUST_BE_POSTIIVE;
402 GlobalVariables.getMessageMap().putError(propertyName, errorKey);
403 }
404 return isValid;
405 } else {
406 if(ObjectUtils.isNull(nonAppliedHolding.getFinancialDocumentLineAmount()) || KualiDecimal.ZERO.equals(nonAppliedHolding.getFinancialDocumentLineAmount())) {
407 // All's OK. Both customer number and amount are empty/null.
408 return true;
409 } else {
410 // Error. Customer number is empty but amount wasn't.
411 String propertyName = ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_CUSTOMER_NUMBER;
412 String errorKey = ArKeyConstants.PaymentApplicationDocumentErrors.UNAPPLIED_AMOUNT_CANNOT_BE_EMPTY_OR_ZERO;
413 GlobalVariables.getMessageMap().putError(propertyName, errorKey);
414 return false;
415 }
416 }
417 }
418
419 /**
420 * This method sums the amounts for a List of NonInvoiceds.
421 * This is used separately from PaymentApplicationDocument.getTotalUnapplied()
422 *
423 * @return
424 */
425 private static KualiDecimal getSumOfNonInvoiceds(List<NonInvoiced> nonInvoiceds) {
426 KualiDecimal sum = new KualiDecimal(0);
427 for(NonInvoiced nonInvoiced : nonInvoiceds) {
428 sum = sum.add(nonInvoiced.getFinancialDocumentLineAmount());
429 }
430 return sum;
431 }
432
433 }