/*
* 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.math.BigDecimal;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.module.ar.ArConstants;
import org.kuali.kfs.module.ar.ArKeyConstants;
import org.kuali.kfs.module.ar.ArPropertyConstants;
import org.kuali.kfs.module.ar.businessobject.CustomerCreditMemoDetail;
import org.kuali.kfs.module.ar.document.CustomerCreditMemoDocument;
import org.kuali.kfs.module.ar.document.CustomerInvoiceDocument;
import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDocumentService;
import org.kuali.kfs.module.ar.document.validation.ContinueCustomerCreditMemoDocumentRule;
import org.kuali.kfs.module.ar.document.validation.RecalculateCustomerCreditMemoDetailRule;
import org.kuali.kfs.module.ar.document.validation.RecalculateCustomerCreditMemoDocumentRule;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kew.api.WorkflowDocumentFactory;
import org.kuali.rice.kns.rules.TransactionalDocumentRuleBase;
import org.kuali.rice.krad.document.Document;
import org.kuali.rice.krad.document.TransactionalDocument;
import org.kuali.rice.krad.service.BusinessObjectService;
import org.kuali.rice.krad.util.GlobalVariables;
import org.kuali.rice.krad.util.KRADConstants;
import org.kuali.rice.krad.util.ObjectUtils;
/**
* This class holds the business rules for the AR Credit Memo Document
*/
public class CustomerCreditMemoDocumentRule extends TransactionalDocumentRuleBase implements RecalculateCustomerCreditMemoDetailRule<TransactionalDocument>, RecalculateCustomerCreditMemoDocumentRule<TransactionalDocument>, ContinueCustomerCreditMemoDocumentRule<TransactionalDocument> {
protected static final BigDecimal ALLOWED_QTY_DEVIATION = new BigDecimal("0.10");
public CustomerCreditMemoDocumentRule() {
}
/**
* @see org.kuali.rice.krad.rules.DocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.krad.document.Document)
*/
@Override
protected boolean processCustomSaveDocumentBusinessRules(Document document) {
boolean isValid = super.processCustomSaveDocumentBusinessRules(document);
GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
isValid &= processRecalculateCustomerCreditMemoDocumentRules((TransactionalDocument) document, true);
GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
return isValid;
}
/**
* @see org.kuali.kfs.module.ar.document.validation.RecalculateCustomerCreditMemoDetailRule#processRecalculateCustomerCreditMemoDetailRules(org.kuali.kfs.sys.document.AccountingDocument,
* org.kuali.kfs.module.ar.businessobject.CustomerCreditMemoDetail)
*/
@Override
public boolean processRecalculateCustomerCreditMemoDetailRules(TransactionalDocument document, CustomerCreditMemoDetail customerCreditMemoDetail) {
boolean success = true;
CustomerCreditMemoDocument customerCreditMemoDocument = (CustomerCreditMemoDocument) document;
customerCreditMemoDocument.refreshReferenceObject("invoice");
String inputKey = isQtyOrItemAmountEntered(customerCreditMemoDetail);
// refresh InvoiceOpenItemAmount and InvoiceOpenAmountQuantity if changed by any other transaction
customerCreditMemoDetail.setInvoiceOpenItemAmount(customerCreditMemoDetail.getCustomerInvoiceDetail().getAmountOpen());
customerCreditMemoDetail.setInvoiceOpenItemQuantity(customerCreditMemoDocument.getInvoiceOpenItemQuantity(customerCreditMemoDetail, customerCreditMemoDetail.getCustomerInvoiceDetail()));
// 'Qty' was entered
if (StringUtils.equals(ArConstants.CustomerCreditMemoConstants.CUSTOMER_CREDIT_MEMO_ITEM_QUANTITY, inputKey)) {
success &= isValueGreaterThanZero(customerCreditMemoDetail.getCreditMemoItemQuantity());
success &= isCustomerCreditMemoQtyLessThanEqualToInvoiceOpenQty(customerCreditMemoDetail);
}
// 'Item Amount' was entered
else if (StringUtils.equals(ArConstants.CustomerCreditMemoConstants.CUSTOMER_CREDIT_MEMO_ITEM_TOTAL_AMOUNT, inputKey)) {
success &= isValueGreaterThanZero(customerCreditMemoDetail.getCreditMemoItemTotalAmount());
success &= isCustomerCreditMemoItemAmountLessThanEqualToInvoiceOpenItemAmount(customerCreditMemoDocument, customerCreditMemoDetail);
}
// both 'Qty' and 'Item Amount' were entered -> validate
else if (StringUtils.equals(ArConstants.CustomerCreditMemoConstants.BOTH_QUANTITY_AND_ITEM_TOTAL_AMOUNT_ENTERED, inputKey)) {
success &= isValueGreaterThanZero(customerCreditMemoDetail.getCreditMemoItemTotalAmount());
success &= isCustomerCreditMemoItemAmountLessThanEqualToInvoiceOpenItemAmount(customerCreditMemoDocument, customerCreditMemoDetail);
success &= isValueGreaterThanZero(customerCreditMemoDetail.getCreditMemoItemQuantity());
success &= isCustomerCreditMemoQtyLessThanEqualToInvoiceOpenQty(customerCreditMemoDetail);
success &= checkIfCustomerCreditMemoQtyAndCustomerCreditMemoItemAmountValid(customerCreditMemoDetail, customerCreditMemoDetail.getCustomerInvoiceDetail().getInvoiceItemUnitPrice());
}
// if there is no input -> wrong input
else {
success = false;
}
return success;
}
public String isQtyOrItemAmountEntered(CustomerCreditMemoDetail customerCreditMemoDetail) {
BigDecimal customerCreditMemoItemQty = customerCreditMemoDetail.getCreditMemoItemQuantity();
KualiDecimal customerCreditMemoItemAmount = customerCreditMemoDetail.getCreditMemoItemTotalAmount();
String inputKey = "";
if (ObjectUtils.isNotNull(customerCreditMemoItemQty) && ObjectUtils.isNotNull(customerCreditMemoItemAmount)) {
inputKey = ArConstants.CustomerCreditMemoConstants.BOTH_QUANTITY_AND_ITEM_TOTAL_AMOUNT_ENTERED;
}
else if (ObjectUtils.isNotNull(customerCreditMemoItemQty)) {
inputKey = ArConstants.CustomerCreditMemoConstants.CUSTOMER_CREDIT_MEMO_ITEM_QUANTITY;
}
else if (ObjectUtils.isNotNull(customerCreditMemoItemAmount)) {
inputKey = ArConstants.CustomerCreditMemoConstants.CUSTOMER_CREDIT_MEMO_ITEM_TOTAL_AMOUNT;
}
return inputKey;
}
public boolean isValueGreaterThanZero(BigDecimal value) {
boolean validValue = (value.compareTo(BigDecimal.ZERO) == 1 ? true : false);
if (!validValue) {
GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_QUANTITY, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_ITEM_QUANTITY_LESS_THAN_OR_EQUAL_TO_ZERO);
}
return validValue;
}
public boolean isValueGreaterThanZero(KualiDecimal value) {
boolean validValue = value.isPositive();
if (!validValue) {
GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_TOTAL_AMOUNT, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_ITEM_AMOUNT_LESS_THAN_OR_EQUAL_TO_ZERO);
}
return validValue;
}
public boolean isCustomerCreditMemoItemAmountLessThanEqualToInvoiceOpenItemAmount(CustomerCreditMemoDocument customerCreditMemoDocument, CustomerCreditMemoDetail customerCreditMemoDetail) {
KualiDecimal invoiceOpenItemAmount = customerCreditMemoDetail.getInvoiceOpenItemAmount();
KualiDecimal creditMemoItemAmount = customerCreditMemoDetail.getCreditMemoItemTotalAmount();
boolean validItemAmount = creditMemoItemAmount.isLessEqual(invoiceOpenItemAmount);
if (!validItemAmount) {
GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_TOTAL_AMOUNT, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_ITEM_AMOUNT_GREATER_THAN_INVOICE_ITEM_AMOUNT);
}
return validItemAmount;
}
public boolean isCustomerCreditMemoQtyLessThanEqualToInvoiceOpenQty(CustomerCreditMemoDetail customerCreditMemoDetail) {
BigDecimal invoiceOpenItemQty = customerCreditMemoDetail.getInvoiceOpenItemQuantity();
BigDecimal customerCreditMemoItemQty = customerCreditMemoDetail.getCreditMemoItemQuantity();
// customer credit memo quantity must not be greater than invoice open item quantity
boolean validQuantity = (customerCreditMemoItemQty.compareTo(invoiceOpenItemQty) < 1 ? true : false);
if (!validQuantity) {
GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_QUANTITY, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_ITEM_QUANTITY_GREATER_THAN_INVOICE_ITEM_QUANTITY);
}
return validQuantity;
}
public boolean checkIfCustomerCreditMemoQtyAndCustomerCreditMemoItemAmountValid(CustomerCreditMemoDetail customerCreditMemoDetail, BigDecimal unitPrice) {
KualiDecimal creditAmount = customerCreditMemoDetail.getCreditMemoItemTotalAmount();
BigDecimal creditQuantity = customerCreditMemoDetail.getCreditMemoItemQuantity();
// if unit price is zero, leave this validation, as it will cause an exception below by attempting to divide by zero
if (unitPrice.compareTo(BigDecimal.ZERO) == 0) {
// no need to report error, because it is already recorded by another validation check.
return false;
}
// determine the expected exact total credit memo quantity, based on actual credit amount entered
BigDecimal expectedCreditQuantity = creditAmount.bigDecimalValue().divide(unitPrice, ArConstants.ITEM_QUANTITY_SCALE, BigDecimal.ROUND_HALF_UP);
// return false if the expected quantity is 0 while the actual quantity is not
if (expectedCreditQuantity.compareTo(BigDecimal.ZERO) == 0 && creditQuantity.compareTo(BigDecimal.ZERO) != 0) {
return false;
}
// determine the deviation percentage that the actual creditQuantity has from expectedCreditQuantity
BigDecimal deviationPercentage = creditQuantity.subtract(expectedCreditQuantity).divide(expectedCreditQuantity, ArConstants.ITEM_QUANTITY_SCALE, BigDecimal.ROUND_HALF_UP).abs();
// only allow a certain deviation of creditQuantity from the expectedCreditQuantity
boolean validFlag = deviationPercentage.compareTo(ALLOWED_QTY_DEVIATION) < 1;
if (!validFlag) {
GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_QUANTITY, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_INVALID_DATA_INPUT);
GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_TOTAL_AMOUNT, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_INVALID_DATA_INPUT);
}
return validFlag;
}
/**
* @see org.kuali.kfs.module.ar.document.validation.RecalculateCustomerCreditMemoDocumentRule#processRecalculateCustomerCreditMemoDocumentRules(org.kuali.kfs.sys.document.AccountingDocument)
*/
@Override
public boolean processRecalculateCustomerCreditMemoDocumentRules(TransactionalDocument document, boolean printErrMsgFlag) {
boolean success = true;
boolean crmDataEnteredFlag = false;
CustomerCreditMemoDocument customerCreditMemoDocument = (CustomerCreditMemoDocument) document;
List<CustomerCreditMemoDetail> customerCreditMemoDetails = customerCreditMemoDocument.getCreditMemoDetails();
int i = 0;
String propertyName;
for (CustomerCreditMemoDetail customerCreditMemoDetail : customerCreditMemoDetails) {
propertyName = KFSConstants.CUSTOMER_CREDIT_MEMO_DETAIL_PROPERTY_NAME + "[" + i + "]";
GlobalVariables.getMessageMap().addToErrorPath(propertyName);
// validate only if there is input data
if (!isQtyOrItemAmountEntered(customerCreditMemoDetail).equals(StringUtils.EMPTY)) {
crmDataEnteredFlag = true;
success &= processRecalculateCustomerCreditMemoDetailRules(customerCreditMemoDocument, customerCreditMemoDetail);
}
GlobalVariables.getMessageMap().removeFromErrorPath(propertyName);
i++;
}
success &= crmDataEnteredFlag;
// print error message if 'Submit'/'Save'/'Blanket Approved' button is pressed and there is no CRM data entered
if (!crmDataEnteredFlag && printErrMsgFlag) {
GlobalVariables.getMessageMap().putError(KFSConstants.DOCUMENT_PROPERTY_NAME, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_NO_DATA_TO_SUBMIT);
}
return success;
}
/**
* @see org.kuali.kfs.module.ar.document.validation.ContinueCustomerCreditMemoDocumentRule#processContinueCustomerCreditMemoDocumentRules(org.kuali.kfs.sys.document.AccountingDocument)
*/
@Override
public boolean processContinueCustomerCreditMemoDocumentRules(TransactionalDocument document) {
boolean success;
CustomerCreditMemoDocument customerCreditMemoDocument = (CustomerCreditMemoDocument) document;
success = checkIfInvoiceNumberIsFinal(customerCreditMemoDocument.getFinancialDocumentReferenceInvoiceNumber());
if (success) {
success = checkIfThereIsNoAnotherCRMInRouteForTheInvoice(customerCreditMemoDocument.getFinancialDocumentReferenceInvoiceNumber());
}
if (success) {
success = checkInvoiceForErrorCorrection(customerCreditMemoDocument.getFinancialDocumentReferenceInvoiceNumber());
}
return success;
}
public boolean checkIfInvoiceNumberIsFinal(String invDocumentNumber) {
boolean success = true;
if (StringUtils.isBlank(invDocumentNumber)) {
success &= false;
GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT__INVOICE_DOCUMENT_NUMBER_IS_REQUIRED);
}
else {
CustomerInvoiceDocumentService service = SpringContext.getBean(CustomerInvoiceDocumentService.class);
CustomerInvoiceDocument customerInvoiceDocument = service.getInvoiceByInvoiceDocumentNumber(invDocumentNumber);
if (ObjectUtils.isNull(customerInvoiceDocument)) {
success &= false;
GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_INVALID_INVOICE_DOCUMENT_NUMBER);
}
else if (!SpringContext.getBean(CustomerInvoiceDocumentService.class).checkIfInvoiceNumberIsFinal(invDocumentNumber)) {
GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_INVOICE_DOCUMENT_NOT_FINAL);
success &= false;
}
}
return success;
}
/**
* This method checks if there is no another CRM in route for the invoice not in route if CRM status is one of the following:
* processed, cancelled, or disapproved
*
* @param invoice
* @return
*/
public boolean checkIfThereIsNoAnotherCRMInRouteForTheInvoice(String invoiceDocumentNumber) {
WorkflowDocument workflowDocument;
boolean success = true;
Map<String, String> fieldValues = new HashMap<String, String>();
fieldValues.put("financialDocumentReferenceInvoiceNumber", invoiceDocumentNumber);
BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class);
Collection<CustomerCreditMemoDocument> customerCreditMemoDocuments = businessObjectService.findMatching(CustomerCreditMemoDocument.class, fieldValues);
// no CRMs associated with the invoice are found
if (customerCreditMemoDocuments.isEmpty()) {
return success;
}
String userId = GlobalVariables.getUserSession().getPrincipalId();
for(CustomerCreditMemoDocument customerCreditMemoDocument : customerCreditMemoDocuments) {
workflowDocument = WorkflowDocumentFactory.loadDocument(userId, customerCreditMemoDocument.getDocumentNumber());
if (!(workflowDocument.isApproved() || workflowDocument.isProcessed() || workflowDocument.isCanceled() || workflowDocument.isDisapproved())) {
GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_ONE_CRM_IN_ROUTE_PER_INVOICE);
success = false;
break;
}
}
return success;
}
/**
* This method checks if the Invoice has been error corrected or is an error correcting invoice
*
* @param invoice
* @return
*/
public boolean checkInvoiceForErrorCorrection(String invoiceDocumentNumber) {
CustomerInvoiceDocumentService service = SpringContext.getBean(CustomerInvoiceDocumentService.class);
CustomerInvoiceDocument customerInvoiceDocument = service.getInvoiceByInvoiceDocumentNumber(invoiceDocumentNumber);
// invoice has been corrected
if (customerInvoiceDocument.hasInvoiceBeenCorrected()) {
GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_CORRECTED_INVOICE);
return false;
}
// this is a correcting invoice
if (customerInvoiceDocument.isInvoiceReversal()) {
GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_CORRECTING_INVOICE);
return false;
}
return true;
}
@Override
public boolean isDocumentAttributesValid(Document document, boolean validateRequired) {
//refresh GLPE nonupdateable business object references....
CustomerCreditMemoDocument customerCreditMemoDocument = (CustomerCreditMemoDocument) document;
for (CustomerCreditMemoDetail customerDetail : customerCreditMemoDocument.getCreditMemoDetails()) {
customerDetail.getCustomerInvoiceDetail().refreshNonUpdateableReferences();
}
for (GeneralLedgerPendingEntry glpe : customerCreditMemoDocument.getGeneralLedgerPendingEntries()) {
glpe.refreshNonUpdateableReferences();
}
return super.isDocumentAttributesValid(document, validateRequired);
}
}