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 }