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 }