/* * 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.ar.document.validation.impl; import java.text.MessageFormat; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.coa.businessobject.Account; import org.kuali.kfs.coa.businessobject.Chart; import org.kuali.kfs.coa.businessobject.ObjectCode; import org.kuali.kfs.coa.businessobject.ProjectCode; import org.kuali.kfs.coa.businessobject.SubAccount; import org.kuali.kfs.coa.businessobject.SubObjectCode; import org.kuali.kfs.module.ar.ArKeyConstants; import org.kuali.kfs.module.ar.ArPropertyConstants; import org.kuali.kfs.module.ar.businessobject.CustomerInvoiceDetail; import org.kuali.kfs.module.ar.businessobject.InvoicePaidApplied; import org.kuali.kfs.module.ar.businessobject.NonAppliedHolding; import org.kuali.kfs.module.ar.businessobject.NonInvoiced; import org.kuali.kfs.module.ar.document.CashControlDocument; import org.kuali.kfs.module.ar.document.PaymentApplicationDocument; import org.kuali.kfs.sys.context.SpringContext; import org.kuali.rice.core.api.util.type.KualiDecimal; import org.kuali.rice.kew.api.exception.WorkflowException; import org.kuali.rice.kns.service.DictionaryValidationService; import org.kuali.rice.krad.service.BusinessObjectService; import org.kuali.rice.krad.util.GlobalVariables; import org.kuali.rice.krad.util.MessageMap; import org.kuali.rice.krad.util.ObjectUtils; /** * Document Rule Utility class for Payment Application. */ public class PaymentApplicationDocumentRuleUtil { private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PaymentApplicationDocumentRuleUtil.class); /** * @param applicationDocument * @param invoiceDetails * @param newNonInvoiced * @param totalFromControl * @return * @throws WorkflowException */ public static boolean validateAllAmounts(PaymentApplicationDocument applicationDocument, List<CustomerInvoiceDetail> invoiceDetails, NonInvoiced newNonInvoiced, KualiDecimal totalFromControl) throws WorkflowException { boolean isValid = validateApplieds(invoiceDetails, applicationDocument, totalFromControl); isValid &= validateNonAppliedHolding(applicationDocument, totalFromControl); isValid &= validateNonInvoiced(newNonInvoiced, applicationDocument, totalFromControl); return isValid; } /** * This method checks that an invoice paid applied is for a valid amount. * * @param invoicePaidApplied * @return */ public static boolean validateInvoicePaidApplied(InvoicePaidApplied invoicePaidApplied, String fieldName, PaymentApplicationDocument document) { boolean isValid = true; invoicePaidApplied.refreshReferenceObject("invoiceDetail"); if(ObjectUtils.isNull(invoicePaidApplied) || ObjectUtils.isNull(invoicePaidApplied.getInvoiceDetail())) { return true; } KualiDecimal amountOwed = invoicePaidApplied.getInvoiceDetail().getAmountOpen(); KualiDecimal amountPaid = invoicePaidApplied.getInvoiceItemAppliedAmount(); if (ObjectUtils.isNull(amountOwed)) { amountOwed = KualiDecimal.ZERO; } if (ObjectUtils.isNull(amountPaid)) { amountPaid = KualiDecimal.ZERO; } // Can't pay more than you owe. if (!amountPaid.isLessEqual(amountOwed)) { isValid = false; LOG.debug("InvoicePaidApplied is not valid. Amount to be applied exceeds amount outstanding."); GlobalVariables.getMessageMap().putError( fieldName, ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_EXCEEDS_AMOUNT_OUTSTANDING); } // Can't apply more than the amount received via the related CashControlDocument if (amountPaid.isGreaterThan(document.getTotalFromControl())) { isValid = false; LOG.debug("InvoicePaidApplied is not valid. Cannot apply more than cash control total amount."); GlobalVariables.getMessageMap().putError( fieldName,ArKeyConstants.PaymentApplicationDocumentErrors.CANNOT_APPLY_MORE_THAN_CASH_CONTROL_TOTAL_AMOUNT); } // cant apply negative amounts if (amountPaid.isNegative() && !document.hasCashControlDocument()) { isValid = false; LOG.debug("InvoicePaidApplied is not valid. Amount to be applied must be positive."); GlobalVariables.getMessageMap().putError( fieldName,ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_MUST_BE_POSTIIVE); } return isValid; } /** * The sum of invoice paid applied amounts cannot exceed the cash control total amount * * @param paymentApplicationDocument * @return * @throws WorkflowException */ public static boolean validateCumulativeSumOfInvoicePaidAppliedDoesntExceedCashControlTotal(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException { KualiDecimal appliedTotal = new KualiDecimal(0); for (InvoicePaidApplied invoicePaidApplied : paymentApplicationDocument.getInvoicePaidApplieds()) { invoicePaidApplied.refreshReferenceObject("invoiceDetail"); appliedTotal = appliedTotal.add(invoicePaidApplied.getInvoiceItemAppliedAmount()); } return paymentApplicationDocument.getTotalFromControl().isGreaterEqual(appliedTotal); } /** * The sum of invoice paid applied amounts cannot be less than zero. * * @param paymentApplicationDocument * @return * @throws WorkflowException */ public static boolean validateCumulativeSumOfInvoicePaidAppliedsIsGreaterThanOrEqualToZero(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException { KualiDecimal appliedTotal = new KualiDecimal(0); for (InvoicePaidApplied invoicePaidApplied : paymentApplicationDocument.getInvoicePaidApplieds()) { invoicePaidApplied.refreshReferenceObject("invoiceDetail"); appliedTotal = appliedTotal.add(invoicePaidApplied.getInvoiceItemAppliedAmount()); } return KualiDecimal.ZERO.isLessEqual(appliedTotal); } /** * The sum of non invoiceds must be less than or equal to the cash control total amount * * @param paymentApplicationDocument * @return * @throws WorkflowException */ public static boolean validateNonInvoicedAmountDoesntExceedCashControlTotal(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException { return paymentApplicationDocument.getTotalFromControl().isGreaterEqual(paymentApplicationDocument.getSumOfNonInvoiceds()); } /** * The unapplied amount can't be negative * * @param paymentApplicationDocument * @return * @throws WorkflowException */ public static boolean validateNonInvoicedAmountIsGreaterThanOrEqualToZero(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException { return KualiDecimal.ZERO.isLessEqual(paymentApplicationDocument.getSumOfNonInvoiceds()); } /** * The unapplied amount must be less than or equal to the cash control total amount * * @param paymentApplicationDocument * @return * @throws WorkflowException */ public static boolean validateUnappliedAmountDoesntExceedCashControlTotal(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException { KualiDecimal a = paymentApplicationDocument.getNonAppliedHoldingAmount(); if (null == a) { return true; } return paymentApplicationDocument.getTotalFromControl().isGreaterEqual(a); } /** * The unapplied amount can't be negative * * @param paymentApplicationDocument * @return * @throws WorkflowException */ public static boolean validateUnappliedAmountIsGreaterThanOrEqualToZero(PaymentApplicationDocument paymentApplicationDocument) throws WorkflowException { if (null == paymentApplicationDocument.getNonAppliedHoldingAmount()) { return true; } return KualiDecimal.ZERO.isLessEqual(paymentApplicationDocument.getNonAppliedHoldingAmount()); } /** * Validate non-ar/non-invoice line items on a PaymentApplicationDocument. * * @param nonInvoiced * @return */ public static boolean validateNonInvoiced(NonInvoiced nonInvoiced, PaymentApplicationDocument paymentApplicationDocument, KualiDecimal totalFromControl) throws WorkflowException { MessageMap errorMap = GlobalVariables.getMessageMap(); int originalErrorCount = errorMap.getErrorCount(); // validate the NonInvoiced BO String sNonInvoicedErrorPath = "nonInvoicedAddLine"; errorMap.addToErrorPath(sNonInvoicedErrorPath); SpringContext.getBean(DictionaryValidationService.class).validateBusinessObject(nonInvoiced); errorMap.removeFromErrorPath(sNonInvoicedErrorPath); if (errorMap.getErrorCount() != originalErrorCount) { return false; } boolean isValid = true; // Required fields, so always validate these. nonInvoiced.refreshReferenceObject("account"); if ( ObjectUtils.isNull(nonInvoiced.getAccount())) { isValid &= false; errorMap.putError( ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_ACCOUNT, ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_ACCOUNT_INVALID, nonInvoiced.getAccountNumber()); } isValid &= validateNonInvoicedLineItem("chartOfAccountsCode", nonInvoiced.getChartOfAccountsCode(), Chart.class, ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_CHART, ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_CHART_INVALID); isValid &= validateNonInvoicedLineItem("accountNumber", nonInvoiced.getAccountNumber(), Account.class, ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_ACCOUNT, ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_ACCOUNT_INVALID); isValid &= validateNonInvoicedLineItem("financialObjectCode", nonInvoiced.getFinancialObjectCode(), ObjectCode.class, ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_OBJECT, ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_OBJECT_CODE_INVALID); // Optional fields, so only validate if a value was entered. if (StringUtils.isNotBlank(nonInvoiced.getSubAccountNumber())) { isValid &= validateNonInvoicedLineItem("subAccountNumber", nonInvoiced.getSubAccountNumber(), SubAccount.class, ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_SUBACCOUNT, ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_SUB_ACCOUNT_INVALID); } if (StringUtils.isNotBlank(nonInvoiced.getFinancialSubObjectCode())) { isValid &= validateNonInvoicedLineItem("financialSubObjectCode", nonInvoiced.getFinancialSubObjectCode(), SubObjectCode.class, ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_SUBOBJECT, ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_SUB_OBJECT_CODE_INVALID); } if (StringUtils.isNotBlank(nonInvoiced.getProjectCode())) { isValid &= validateNonInvoicedLineItem("code", nonInvoiced.getProjectCode(), ProjectCode.class, ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_PROJECT, ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_PROJECT_CODE_INVALID); } isValid &= validateNonInvoicedLineAmount(nonInvoiced, paymentApplicationDocument, totalFromControl); return isValid; } /** * This method validates the provided non invoiced line value. * * @param attributeName The name of the attribute as it is defined within its parent business object (ie. financialObjectCode in * ObjectCode.java) * @param value The value of the NonInvoiced line to be validated. * @param boClass The class that the provided value represents (ie. accountNumber represents Account.class) * @param errorPropertyName The Payment Application document property name to be used for applying errors when necessary. * @param errorMessageKey The error key path to be used for applying errors when necessary. * @return True if the value provided is valid and exists, false otherwise. */ protected static boolean validateNonInvoicedLineItem(String attributeName, String value, Class boClass, String errorPropertyName, String errorMessageKey) { MessageMap errorMap = GlobalVariables.getMessageMap(); boolean isValid = true; Map<String, String> criteria = new HashMap<String, String>(); criteria.put(attributeName, value); Object object = SpringContext.getBean(BusinessObjectService.class).findByPrimaryKey(boClass, criteria); if(ObjectUtils.isNull(object)) { errorMap.putError(errorPropertyName, errorMessageKey, value); isValid &= false; } return isValid; } /** * @param nonInvoiced * @param paymentApplicationDocument * @param totalFromControl * @return */ protected static boolean validateNonInvoicedLineAmount(NonInvoiced nonInvoiced, PaymentApplicationDocument paymentApplicationDocument, KualiDecimal totalFromControl) { MessageMap errorMap = GlobalVariables.getMessageMap(); KualiDecimal nonArLineAmount = nonInvoiced.getFinancialDocumentLineAmount(); // check that dollar amount is not zero before continuing if (ObjectUtils.isNull(nonArLineAmount)) { errorMap.putError(ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_AMOUNT, ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_AMOUNT_REQUIRED); return false; } else { KualiDecimal cashControlBalanceToBeApplied = totalFromControl; cashControlBalanceToBeApplied = cashControlBalanceToBeApplied.add(paymentApplicationDocument.getTotalFromControl()); cashControlBalanceToBeApplied.subtract(paymentApplicationDocument.getTotalApplied()); cashControlBalanceToBeApplied.subtract(paymentApplicationDocument.getNonAppliedHoldingAmount()); if (nonArLineAmount.isZero()) { errorMap.putError(ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_AMOUNT, ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_CANNOT_BE_ZERO); return false; } else if (nonArLineAmount.isNegative()) { errorMap.putError(ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_AMOUNT, ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_MUST_BE_POSTIIVE); return false; } // check that we're not trying to apply more funds to the invoice than the invoice has balance (ie, over-applying) else if (KualiDecimal.ZERO.isGreaterThan(cashControlBalanceToBeApplied.subtract(nonArLineAmount))) { errorMap.putError(ArPropertyConstants.PaymentApplicationDocumentFields.NON_INVOICED_LINE_AMOUNT, ArKeyConstants.PaymentApplicationDocumentErrors.NON_AR_AMOUNT_EXCEEDS_BALANCE_TO_BE_APPLIED); return false; } } return true; } /** * This method determines whether or not the amount to be applied to an invoice is acceptable. * * @param customerInvoiceDetails * @return */ public static boolean validateApplieds(List<CustomerInvoiceDetail> customerInvoiceDetails, PaymentApplicationDocument paymentAplicationDocument, KualiDecimal totalFromControl) throws WorkflowException { // Indicates whether the validation succeeded boolean isValid = true; // Figure out the maximum we should be able to apply. for (CustomerInvoiceDetail customerInvoiceDetail : customerInvoiceDetails) { isValid &= validateAmountAppliedToCustomerInvoiceDetailByPaymentApplicationDocument(customerInvoiceDetail, paymentAplicationDocument, totalFromControl); } return isValid; } /** * @param customerInvoiceDetail * @param paymentApplicationDocument * @return */ public static boolean validateAmountAppliedToCustomerInvoiceDetailByPaymentApplicationDocument(CustomerInvoiceDetail customerInvoiceDetail, PaymentApplicationDocument paymentApplicationDocument, KualiDecimal totalFromControl) throws WorkflowException { boolean isValid = true; // This let's us highlight a specific invoice detail line String propertyName = MessageFormat.format(ArPropertyConstants.PaymentApplicationDocumentFields.AMOUNT_TO_BE_APPLIED_LINE_N, customerInvoiceDetail.getSequenceNumber().toString()); KualiDecimal amountAppliedByAllOtherDocuments = customerInvoiceDetail.getAmountAppliedExcludingAnyAmountAppliedBy(paymentApplicationDocument.getDocumentNumber()); KualiDecimal amountAppliedByThisDocument = customerInvoiceDetail.getAmountAppliedBy(paymentApplicationDocument.getDocumentNumber()); KualiDecimal totalAppliedAmount = amountAppliedByAllOtherDocuments.add(amountAppliedByThisDocument); // Can't apply more than the total amount of the detail if (!totalAppliedAmount.isLessEqual(totalFromControl)) { isValid = false; GlobalVariables.getMessageMap().putError(propertyName, ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_EXCEEDS_AMOUNT_OUTSTANDING); } // Can't apply a negative amount. if (KualiDecimal.ZERO.isGreaterThan(amountAppliedByThisDocument)) { isValid = false; GlobalVariables.getMessageMap().putError(propertyName, ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_MUST_BE_GREATER_THAN_ZERO); } // Can't apply more than the total amount outstanding on the cash control document. CashControlDocument cashControlDocument = paymentApplicationDocument.getCashControlDocument(); if (ObjectUtils.isNotNull(cashControlDocument)) { if (cashControlDocument.getCashControlTotalAmount().isLessThan(amountAppliedByThisDocument)) { isValid = false; GlobalVariables.getMessageMap().putError(propertyName, ArKeyConstants.PaymentApplicationDocumentErrors.CANNOT_APPLY_MORE_THAN_BALANCE_TO_BE_APPLIED); } } return isValid; } /** * This method validates the unapplied attribute of the document. * * @param document * @return * @throws WorkflowException */ public static boolean validateNonAppliedHolding(PaymentApplicationDocument applicationDocument, KualiDecimal totalFromControl) throws WorkflowException { NonAppliedHolding nonAppliedHolding = applicationDocument.getNonAppliedHolding(); if (ObjectUtils.isNull(nonAppliedHolding)) { return true; } if (StringUtils.isNotEmpty(nonAppliedHolding.getCustomerNumber())) { KualiDecimal nonAppliedAmount = nonAppliedHolding.getFinancialDocumentLineAmount(); if (null == nonAppliedAmount) { nonAppliedAmount = KualiDecimal.ZERO; } boolean isValid = totalFromControl.isGreaterEqual(nonAppliedAmount); if (!isValid) { String propertyName = ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_AMOUNT; String errorKey = ArKeyConstants.PaymentApplicationDocumentErrors.UNAPPLIED_AMOUNT_CANNOT_EXCEED_AVAILABLE_AMOUNT; GlobalVariables.getMessageMap().putError(propertyName, errorKey); } // The amount of the unapplied can't exceed the remaining balance to be applied KualiDecimal totalBalanceToBeApplied = applicationDocument.getUnallocatedBalance(); isValid = KualiDecimal.ZERO.isLessEqual(totalBalanceToBeApplied); if (!isValid) { String propertyName = ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_AMOUNT; String errorKey = ArKeyConstants.PaymentApplicationDocumentErrors.UNAPPLIED_AMOUNT_CANNOT_EXCEED_BALANCE_TO_BE_APPLIED; GlobalVariables.getMessageMap().putError(propertyName, errorKey); } // the unapplied amount cannot be negative isValid = nonAppliedAmount.isPositive() || nonAppliedAmount.isZero(); if (!isValid) { String propertyName = ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_AMOUNT; String errorKey = ArKeyConstants.PaymentApplicationDocumentErrors.AMOUNT_TO_BE_APPLIED_MUST_BE_POSTIIVE; GlobalVariables.getMessageMap().putError(propertyName, errorKey); } return isValid; } else { if (ObjectUtils.isNull(nonAppliedHolding.getFinancialDocumentLineAmount()) || KualiDecimal.ZERO.equals(nonAppliedHolding.getFinancialDocumentLineAmount())) { // All's OK. Both customer number and amount are empty/null. return true; } else { // Error. Customer number is empty but amount wasn't. String propertyName = ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_CUSTOMER_NUMBER; String errorKey = ArKeyConstants.PaymentApplicationDocumentErrors.UNAPPLIED_AMOUNT_CANNOT_BE_EMPTY_OR_ZERO; GlobalVariables.getMessageMap().putError(propertyName, errorKey); return false; } } } }