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.math.BigDecimal; 019 import java.util.Collection; 020 import java.util.HashMap; 021 import java.util.List; 022 import java.util.Map; 023 024 import org.apache.commons.lang.StringUtils; 025 import org.kuali.kfs.module.ar.ArConstants; 026 import org.kuali.kfs.module.ar.ArKeyConstants; 027 import org.kuali.kfs.module.ar.ArPropertyConstants; 028 import org.kuali.kfs.module.ar.businessobject.CustomerCreditMemoDetail; 029 import org.kuali.kfs.module.ar.document.CustomerCreditMemoDocument; 030 import org.kuali.kfs.module.ar.document.CustomerInvoiceDocument; 031 import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDocumentService; 032 import org.kuali.kfs.module.ar.document.validation.ContinueCustomerCreditMemoDocumentRule; 033 import org.kuali.kfs.module.ar.document.validation.RecalculateCustomerCreditMemoDetailRule; 034 import org.kuali.kfs.module.ar.document.validation.RecalculateCustomerCreditMemoDocumentRule; 035 import org.kuali.kfs.sys.KFSConstants; 036 import org.kuali.kfs.sys.context.SpringContext; 037 import org.kuali.kfs.sys.document.dataaccess.FinancialSystemDocumentHeaderDao; 038 import org.kuali.rice.kew.exception.WorkflowException; 039 import org.kuali.rice.kim.bo.Person; 040 import org.kuali.rice.kns.bo.DocumentHeader; 041 import org.kuali.rice.kns.document.Document; 042 import org.kuali.rice.kns.document.TransactionalDocument; 043 import org.kuali.rice.kns.exception.UnknownDocumentIdException; 044 import org.kuali.rice.kns.rules.TransactionalDocumentRuleBase; 045 import org.kuali.rice.kns.service.BusinessObjectService; 046 import org.kuali.rice.kns.util.GlobalVariables; 047 import org.kuali.rice.kns.util.KNSConstants; 048 import org.kuali.rice.kns.util.KualiDecimal; 049 import org.kuali.rice.kns.util.ObjectUtils; 050 import org.kuali.rice.kns.workflow.service.KualiWorkflowDocument; 051 import org.kuali.rice.kns.workflow.service.WorkflowDocumentService; 052 053 /** 054 * This class holds the business rules for the AR Credit Memo Document 055 */ 056 057 public class CustomerCreditMemoDocumentRule extends TransactionalDocumentRuleBase implements RecalculateCustomerCreditMemoDetailRule<TransactionalDocument>, RecalculateCustomerCreditMemoDocumentRule<TransactionalDocument>, ContinueCustomerCreditMemoDocumentRule<TransactionalDocument> { 058 059 protected static final KualiDecimal ALLOWED_QTY_DEVIATION = new KualiDecimal("0.10"); 060 061 public CustomerCreditMemoDocumentRule() { 062 } 063 064 /** 065 * @see org.kuali.rice.kns.rules.DocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.kns.document.Document) 066 */ 067 protected boolean processCustomSaveDocumentBusinessRules(Document document) { 068 boolean isValid = super.processCustomSaveDocumentBusinessRules(document); 069 070 GlobalVariables.getMessageMap().addToErrorPath(KNSConstants.DOCUMENT_PROPERTY_NAME); 071 isValid &= processRecalculateCustomerCreditMemoDocumentRules((TransactionalDocument) document, true); 072 GlobalVariables.getMessageMap().removeFromErrorPath(KNSConstants.DOCUMENT_PROPERTY_NAME); 073 074 return isValid; 075 } 076 077 /** 078 * @see org.kuali.kfs.module.ar.document.validation.RecalculateCustomerCreditMemoDetailRule#processRecalculateCustomerCreditMemoDetailRules(org.kuali.kfs.sys.document.AccountingDocument, 079 * org.kuali.kfs.module.ar.businessobject.CustomerCreditMemoDetail) 080 */ 081 public boolean processRecalculateCustomerCreditMemoDetailRules(TransactionalDocument document, CustomerCreditMemoDetail customerCreditMemoDetail) { 082 boolean success = true; 083 084 CustomerCreditMemoDocument customerCreditMemoDocument = (CustomerCreditMemoDocument) document; 085 customerCreditMemoDocument.refreshReferenceObject("invoice"); 086 String inputKey = isQtyOrItemAmountEntered(customerCreditMemoDetail); 087 088 // refresh InvoiceOpenItemAmount and InvoiceOpenAmountQuantity if changed by any other transaction 089 customerCreditMemoDetail.setInvoiceOpenItemAmount(customerCreditMemoDetail.getCustomerInvoiceDetail().getAmountOpen()); 090 customerCreditMemoDetail.setInvoiceOpenItemQuantity(customerCreditMemoDocument.getInvoiceOpenItemQuantity(customerCreditMemoDetail, customerCreditMemoDetail.getCustomerInvoiceDetail())); 091 092 // 'Qty' was entered 093 if (StringUtils.equals(ArConstants.CustomerCreditMemoConstants.CUSTOMER_CREDIT_MEMO_ITEM_QUANTITY, inputKey)) { 094 success &= isValueGreaterThanZero(customerCreditMemoDetail.getCreditMemoItemQuantity()); 095 success &= isCustomerCreditMemoQtyLessThanEqualToInvoiceOpenQty(customerCreditMemoDetail); 096 } 097 // 'Item Amount' was entered 098 else if (StringUtils.equals(ArConstants.CustomerCreditMemoConstants.CUSTOMER_CREDIT_MEMO_ITEM_TOTAL_AMOUNT, inputKey)) { 099 success &= isValueGreaterThanZero(customerCreditMemoDetail.getCreditMemoItemTotalAmount()); 100 success &= isCustomerCreditMemoItemAmountLessThanEqualToInvoiceOpenItemAmount(customerCreditMemoDocument, customerCreditMemoDetail); 101 } 102 // both 'Qty' and 'Item Amount' were entered -> validate 103 else if (StringUtils.equals(ArConstants.CustomerCreditMemoConstants.BOTH_QUANTITY_AND_ITEM_TOTAL_AMOUNT_ENTERED, inputKey)) { 104 success &= isValueGreaterThanZero(customerCreditMemoDetail.getCreditMemoItemTotalAmount()); 105 success &= isCustomerCreditMemoItemAmountLessThanEqualToInvoiceOpenItemAmount(customerCreditMemoDocument, customerCreditMemoDetail); 106 success &= isValueGreaterThanZero(customerCreditMemoDetail.getCreditMemoItemQuantity()); 107 success &= isCustomerCreditMemoQtyLessThanEqualToInvoiceOpenQty(customerCreditMemoDetail); 108 success &= checkIfCustomerCreditMemoQtyAndCustomerCreditMemoItemAmountValid(customerCreditMemoDetail, customerCreditMemoDetail.getCustomerInvoiceDetail().getInvoiceItemUnitPrice()); 109 } 110 // if there is no input -> wrong input 111 else { 112 success = false; 113 } 114 return success; 115 } 116 117 public String isQtyOrItemAmountEntered(CustomerCreditMemoDetail customerCreditMemoDetail) { 118 119 BigDecimal customerCreditMemoItemQty = customerCreditMemoDetail.getCreditMemoItemQuantity(); 120 KualiDecimal customerCreditMemoItemAmount = customerCreditMemoDetail.getCreditMemoItemTotalAmount(); 121 String inputKey = ""; 122 123 if (ObjectUtils.isNotNull(customerCreditMemoItemQty) && ObjectUtils.isNotNull(customerCreditMemoItemAmount)) 124 inputKey = ArConstants.CustomerCreditMemoConstants.BOTH_QUANTITY_AND_ITEM_TOTAL_AMOUNT_ENTERED; 125 else if (ObjectUtils.isNotNull(customerCreditMemoItemQty)) 126 inputKey = ArConstants.CustomerCreditMemoConstants.CUSTOMER_CREDIT_MEMO_ITEM_QUANTITY; 127 else if (ObjectUtils.isNotNull(customerCreditMemoItemAmount)) 128 inputKey = ArConstants.CustomerCreditMemoConstants.CUSTOMER_CREDIT_MEMO_ITEM_TOTAL_AMOUNT; 129 130 return inputKey; 131 } 132 133 public boolean isValueGreaterThanZero(BigDecimal value) { 134 boolean validValue = (value.compareTo(BigDecimal.ZERO) == 1 ? true : false); 135 if (!validValue) 136 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_QUANTITY, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_ITEM_QUANTITY_LESS_THAN_OR_EQUAL_TO_ZERO); 137 return validValue; 138 } 139 140 public boolean isValueGreaterThanZero(KualiDecimal value) { 141 boolean validValue = value.isPositive(); 142 if (!validValue) 143 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_TOTAL_AMOUNT, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_ITEM_AMOUNT_LESS_THAN_OR_EQUAL_TO_ZERO); 144 return validValue; 145 } 146 147 public boolean isCustomerCreditMemoItemAmountLessThanEqualToInvoiceOpenItemAmount(CustomerCreditMemoDocument customerCreditMemoDocument, CustomerCreditMemoDetail customerCreditMemoDetail) { 148 149 KualiDecimal invoiceOpenItemAmount = customerCreditMemoDetail.getInvoiceOpenItemAmount(); 150 KualiDecimal creditMemoItemAmount = customerCreditMemoDetail.getCreditMemoItemTotalAmount(); 151 152 boolean validItemAmount = creditMemoItemAmount.isLessEqual(invoiceOpenItemAmount); 153 if (!validItemAmount) 154 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_TOTAL_AMOUNT, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_ITEM_AMOUNT_GREATER_THAN_INVOICE_ITEM_AMOUNT); 155 156 return validItemAmount; 157 } 158 159 public boolean isCustomerCreditMemoQtyLessThanEqualToInvoiceOpenQty(CustomerCreditMemoDetail customerCreditMemoDetail) { 160 KualiDecimal invoiceOpenItemQty = customerCreditMemoDetail.getInvoiceOpenItemQuantity(); 161 KualiDecimal customerCreditMemoItemQty = new KualiDecimal(customerCreditMemoDetail.getCreditMemoItemQuantity()); 162 163 // customer credit memo quantity must not be greater than invoice open item quantity 164 boolean validQuantity = (customerCreditMemoItemQty.compareTo(invoiceOpenItemQty) < 1 ? true : false); 165 if (!validQuantity) 166 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_QUANTITY, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_ITEM_QUANTITY_GREATER_THAN_INVOICE_ITEM_QUANTITY); 167 168 return validQuantity; 169 } 170 171 public boolean checkIfCustomerCreditMemoQtyAndCustomerCreditMemoItemAmountValid(CustomerCreditMemoDetail customerCreditMemoDetail, BigDecimal unitPrice) { 172 KualiDecimal creditAmount = customerCreditMemoDetail.getCreditMemoItemTotalAmount(); 173 KualiDecimal creditQuantity = new KualiDecimal(customerCreditMemoDetail.getCreditMemoItemQuantity()); 174 175 // if unit price is zero, leave this validation, as it will cause an exception below by attempting to divide by zero 176 if (unitPrice.compareTo(BigDecimal.ZERO) == 0) { 177 // no need to report error, because it is already recorded by another validation check. 178 return false; 179 } 180 181 // determine the expected exact total credit memo quantity, based on actual credit amount entered 182 KualiDecimal expectedCreditQuantity = creditAmount.divide(new KualiDecimal(unitPrice), true); 183 if (expectedCreditQuantity == null || expectedCreditQuantity.isZero()) { 184 expectedCreditQuantity = new KualiDecimal(0.01d); 185 return true; 186 } 187 188 // determine the deviation percentage that the actual creditQuantity has from expectedCreditQuantity 189 KualiDecimal deviationPercentage = expectedCreditQuantity.subtract(creditQuantity).abs().divide(expectedCreditQuantity); 190 191 // only allow a certain deviation of creditQuantity from the expectedCreditQuantity 192 boolean validFlag = (deviationPercentage.isLessEqual(ALLOWED_QTY_DEVIATION)); 193 194 if (!validFlag) { 195 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_QUANTITY, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_INVALID_DATA_INPUT); 196 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_TOTAL_AMOUNT, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_INVALID_DATA_INPUT); 197 } 198 return validFlag; 199 } 200 201 /** 202 * @see org.kuali.kfs.module.ar.document.validation.RecalculateCustomerCreditMemoDocumentRule#processRecalculateCustomerCreditMemoDocumentRules(org.kuali.kfs.sys.document.AccountingDocument) 203 */ 204 public boolean processRecalculateCustomerCreditMemoDocumentRules(TransactionalDocument document, boolean printErrMsgFlag) { 205 boolean success = true; 206 boolean crmDataEnteredFlag = false; 207 CustomerCreditMemoDocument customerCreditMemoDocument = (CustomerCreditMemoDocument) document; 208 List<CustomerCreditMemoDetail> customerCreditMemoDetails = customerCreditMemoDocument.getCreditMemoDetails(); 209 int i = 0; 210 String propertyName; 211 212 for (CustomerCreditMemoDetail customerCreditMemoDetail : customerCreditMemoDetails) { 213 propertyName = KFSConstants.CUSTOMER_CREDIT_MEMO_DETAIL_PROPERTY_NAME + "[" + i + "]"; 214 GlobalVariables.getMessageMap().addToErrorPath(propertyName); 215 216 // validate only if there is input data 217 if (!isQtyOrItemAmountEntered(customerCreditMemoDetail).equals(StringUtils.EMPTY)) { 218 crmDataEnteredFlag = true; 219 success &= processRecalculateCustomerCreditMemoDetailRules(customerCreditMemoDocument, customerCreditMemoDetail); 220 } 221 GlobalVariables.getMessageMap().removeFromErrorPath(propertyName); 222 i++; 223 } 224 225 success &= crmDataEnteredFlag; 226 227 // print error message if 'Submit'/'Save'/'Blanket Approved' button is pressed and there is no CRM data entered 228 if (!crmDataEnteredFlag && printErrMsgFlag) 229 GlobalVariables.getMessageMap().putError(KFSConstants.DOCUMENT_PROPERTY_NAME, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_NO_DATA_TO_SUBMIT); 230 231 return success; 232 } 233 234 /** 235 * @see org.kuali.kfs.module.ar.document.validation.ContinueCustomerCreditMemoDocumentRule#processContinueCustomerCreditMemoDocumentRules(org.kuali.kfs.sys.document.AccountingDocument) 236 */ 237 public boolean processContinueCustomerCreditMemoDocumentRules(TransactionalDocument document) { 238 boolean success; 239 CustomerCreditMemoDocument customerCreditMemoDocument = (CustomerCreditMemoDocument) document; 240 241 success = checkIfInvoiceNumberIsFinal(customerCreditMemoDocument.getFinancialDocumentReferenceInvoiceNumber()); 242 if (success) 243 success = checkIfThereIsNoAnotherCRMInRouteForTheInvoice(customerCreditMemoDocument.getFinancialDocumentReferenceInvoiceNumber()); 244 if (success) 245 success = checkInvoiceForErrorCorrection(customerCreditMemoDocument.getFinancialDocumentReferenceInvoiceNumber()); 246 247 return success; 248 } 249 250 public boolean checkIfInvoiceNumberIsFinal(String invDocumentNumber) { 251 boolean success = true; 252 253 if (StringUtils.isBlank(invDocumentNumber)) { 254 success &= false; 255 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT__INVOICE_DOCUMENT_NUMBER_IS_REQUIRED); 256 } 257 else { 258 CustomerInvoiceDocumentService service = SpringContext.getBean(CustomerInvoiceDocumentService.class); 259 CustomerInvoiceDocument customerInvoiceDocument = service.getInvoiceByInvoiceDocumentNumber(invDocumentNumber); 260 if (ObjectUtils.isNull(customerInvoiceDocument)) { 261 success &= false; 262 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_INVALID_INVOICE_DOCUMENT_NUMBER); 263 } 264 else if (!SpringContext.getBean(CustomerInvoiceDocumentService.class).checkIfInvoiceNumberIsFinal(invDocumentNumber)) { 265 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_INVOICE_DOCUMENT_NOT_FINAL); 266 success &= false; 267 } 268 } 269 return success; 270 } 271 272 /** 273 * 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: 274 * processed, cancelled, or disapproved 275 * 276 * @param invoice 277 * @return 278 */ 279 public boolean checkIfThereIsNoAnotherCRMInRouteForTheInvoice(String invoiceDocumentNumber) { 280 281 KualiWorkflowDocument workflowDocument; 282 boolean success = true; 283 284 Map<String, String> fieldValues = new HashMap<String, String>(); 285 fieldValues.put("financialDocumentReferenceInvoiceNumber", invoiceDocumentNumber); 286 287 BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class); 288 Collection<CustomerCreditMemoDocument> customerCreditMemoDocuments = businessObjectService.findMatching(CustomerCreditMemoDocument.class, fieldValues); 289 290 // no CRMs associated with the invoice are found 291 if (customerCreditMemoDocuments.isEmpty()) 292 return success; 293 294 Person user = GlobalVariables.getUserSession().getPerson(); 295 296 for (CustomerCreditMemoDocument customerCreditMemoDocument : customerCreditMemoDocuments) { 297 try { 298 workflowDocument = SpringContext.getBean(WorkflowDocumentService.class).createWorkflowDocument(Long.valueOf(customerCreditMemoDocument.getDocumentNumber()), user); 299 } 300 catch (WorkflowException e) { 301 throw new UnknownDocumentIdException("no document found for documentHeaderId '" + customerCreditMemoDocument.getDocumentNumber() + "'", e); 302 } 303 304 if (!(workflowDocument.stateIsApproved() || workflowDocument.stateIsProcessed() || workflowDocument.stateIsCanceled() || workflowDocument.stateIsDisapproved())) { 305 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_ONE_CRM_IN_ROUTE_PER_INVOICE); 306 success = false; 307 break; 308 } 309 } 310 return success; 311 } 312 313 /** 314 * This method checks if the Invoice has been error corrected or is an error correcting invoice 315 * 316 * @param invoice 317 * @return 318 */ 319 public boolean checkInvoiceForErrorCorrection(String invoiceDocumentNumber) { 320 CustomerInvoiceDocumentService service = SpringContext.getBean(CustomerInvoiceDocumentService.class); 321 CustomerInvoiceDocument customerInvoiceDocument = service.getInvoiceByInvoiceDocumentNumber(invoiceDocumentNumber); 322 323 DocumentHeader documentHeader = SpringContext.getBean(FinancialSystemDocumentHeaderDao.class).getCorrectingDocumentHeader(invoiceDocumentNumber); 324 325 // invoice has been corrected 326 if (ObjectUtils.isNotNull(documentHeader)) { 327 if (StringUtils.isNotBlank(documentHeader.getDocumentNumber())) { 328 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_CORRECTED_INVOICE); 329 return false; 330 } 331 } 332 // this is a correcting invoice 333 if (customerInvoiceDocument.isInvoiceReversal()) { 334 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_CORRECTING_INVOICE); 335 return false; 336 } 337 return true; 338 } 339 340 }