View Javadoc
1   /*
2    * The Kuali Financial System, a comprehensive financial management system for higher education.
3    * 
4    * Copyright 2005-2014 The Kuali Foundation
5    * 
6    * This program is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU Affero General Public License as
8    * published by the Free Software Foundation, either version 3 of the
9    * License, or (at your option) any later version.
10   * 
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU Affero General Public License for more details.
15   * 
16   * You should have received a copy of the GNU Affero General Public License
17   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18   */
19  package org.kuali.kfs.module.ar.document.validation.impl;
20  
21  import java.math.BigDecimal;
22  import java.util.Collection;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  
27  import org.apache.commons.lang.StringUtils;
28  import org.kuali.kfs.module.ar.ArConstants;
29  import org.kuali.kfs.module.ar.ArKeyConstants;
30  import org.kuali.kfs.module.ar.ArPropertyConstants;
31  import org.kuali.kfs.module.ar.businessobject.CustomerCreditMemoDetail;
32  import org.kuali.kfs.module.ar.document.CustomerCreditMemoDocument;
33  import org.kuali.kfs.module.ar.document.CustomerInvoiceDocument;
34  import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDocumentService;
35  import org.kuali.kfs.module.ar.document.validation.ContinueCustomerCreditMemoDocumentRule;
36  import org.kuali.kfs.module.ar.document.validation.RecalculateCustomerCreditMemoDetailRule;
37  import org.kuali.kfs.module.ar.document.validation.RecalculateCustomerCreditMemoDocumentRule;
38  import org.kuali.kfs.sys.KFSConstants;
39  import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
40  import org.kuali.kfs.sys.context.SpringContext;
41  import org.kuali.rice.core.api.util.type.KualiDecimal;
42  import org.kuali.rice.kew.api.WorkflowDocument;
43  import org.kuali.rice.kew.api.WorkflowDocumentFactory;
44  import org.kuali.rice.kns.rules.TransactionalDocumentRuleBase;
45  import org.kuali.rice.krad.document.Document;
46  import org.kuali.rice.krad.document.TransactionalDocument;
47  import org.kuali.rice.krad.service.BusinessObjectService;
48  import org.kuali.rice.krad.util.GlobalVariables;
49  import org.kuali.rice.krad.util.KRADConstants;
50  import org.kuali.rice.krad.util.ObjectUtils;
51  
52  /**
53   * This class holds the business rules for the AR Credit Memo Document
54   */
55  
56  public class CustomerCreditMemoDocumentRule extends TransactionalDocumentRuleBase implements RecalculateCustomerCreditMemoDetailRule<TransactionalDocument>, RecalculateCustomerCreditMemoDocumentRule<TransactionalDocument>, ContinueCustomerCreditMemoDocumentRule<TransactionalDocument> {
57  
58      protected static final BigDecimal ALLOWED_QTY_DEVIATION = new BigDecimal("0.10");
59  
60      public CustomerCreditMemoDocumentRule() {
61      }
62  
63      /**
64       * @see org.kuali.rice.krad.rules.DocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.krad.document.Document)
65       */
66      @Override
67      protected boolean processCustomSaveDocumentBusinessRules(Document document) {
68          boolean isValid = super.processCustomSaveDocumentBusinessRules(document);
69  
70          GlobalVariables.getMessageMap().addToErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
71          isValid &= processRecalculateCustomerCreditMemoDocumentRules((TransactionalDocument) document, true);
72          GlobalVariables.getMessageMap().removeFromErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);
73  
74          return isValid;
75      }
76  
77      /**
78       * @see org.kuali.kfs.module.ar.document.validation.RecalculateCustomerCreditMemoDetailRule#processRecalculateCustomerCreditMemoDetailRules(org.kuali.kfs.sys.document.AccountingDocument,
79       *      org.kuali.kfs.module.ar.businessobject.CustomerCreditMemoDetail)
80       */
81      @Override
82      public boolean processRecalculateCustomerCreditMemoDetailRules(TransactionalDocument document, CustomerCreditMemoDetail customerCreditMemoDetail) {
83          boolean success = true;
84  
85          CustomerCreditMemoDocument customerCreditMemoDocument = (CustomerCreditMemoDocument) document;
86          customerCreditMemoDocument.refreshReferenceObject("invoice");
87          String inputKey = isQtyOrItemAmountEntered(customerCreditMemoDetail);
88  
89          // refresh InvoiceOpenItemAmount and InvoiceOpenAmountQuantity if changed by any other transaction
90          customerCreditMemoDetail.setInvoiceOpenItemAmount(customerCreditMemoDetail.getCustomerInvoiceDetail().getAmountOpen());
91          customerCreditMemoDetail.setInvoiceOpenItemQuantity(customerCreditMemoDocument.getInvoiceOpenItemQuantity(customerCreditMemoDetail, customerCreditMemoDetail.getCustomerInvoiceDetail()));
92  
93          // 'Qty' was entered
94          if (StringUtils.equals(ArConstants.CustomerCreditMemoConstants.CUSTOMER_CREDIT_MEMO_ITEM_QUANTITY, inputKey)) {
95              success &= isValueGreaterThanZero(customerCreditMemoDetail.getCreditMemoItemQuantity());
96              success &= isCustomerCreditMemoQtyLessThanEqualToInvoiceOpenQty(customerCreditMemoDetail);
97          }
98          // 'Item Amount' was entered
99          else if (StringUtils.equals(ArConstants.CustomerCreditMemoConstants.CUSTOMER_CREDIT_MEMO_ITEM_TOTAL_AMOUNT, inputKey)) {
100             success &= isValueGreaterThanZero(customerCreditMemoDetail.getCreditMemoItemTotalAmount());
101             success &= isCustomerCreditMemoItemAmountLessThanEqualToInvoiceOpenItemAmount(customerCreditMemoDocument, customerCreditMemoDetail);
102         }
103         // both 'Qty' and 'Item Amount' were entered -> validate
104         else if (StringUtils.equals(ArConstants.CustomerCreditMemoConstants.BOTH_QUANTITY_AND_ITEM_TOTAL_AMOUNT_ENTERED, inputKey)) {
105             success &= isValueGreaterThanZero(customerCreditMemoDetail.getCreditMemoItemTotalAmount());
106             success &= isCustomerCreditMemoItemAmountLessThanEqualToInvoiceOpenItemAmount(customerCreditMemoDocument, customerCreditMemoDetail);
107             success &= isValueGreaterThanZero(customerCreditMemoDetail.getCreditMemoItemQuantity());
108             success &= isCustomerCreditMemoQtyLessThanEqualToInvoiceOpenQty(customerCreditMemoDetail);
109             success &= checkIfCustomerCreditMemoQtyAndCustomerCreditMemoItemAmountValid(customerCreditMemoDetail, customerCreditMemoDetail.getCustomerInvoiceDetail().getInvoiceItemUnitPrice());
110         }
111         // if there is no input -> wrong input
112         else {
113             success = false;
114         }
115         return success;
116     }
117 
118     public String isQtyOrItemAmountEntered(CustomerCreditMemoDetail customerCreditMemoDetail) {
119 
120         BigDecimal customerCreditMemoItemQty = customerCreditMemoDetail.getCreditMemoItemQuantity();
121         KualiDecimal customerCreditMemoItemAmount = customerCreditMemoDetail.getCreditMemoItemTotalAmount();
122         String inputKey = "";
123 
124         if (ObjectUtils.isNotNull(customerCreditMemoItemQty) && ObjectUtils.isNotNull(customerCreditMemoItemAmount)) {
125             inputKey = ArConstants.CustomerCreditMemoConstants.BOTH_QUANTITY_AND_ITEM_TOTAL_AMOUNT_ENTERED;
126         }
127         else if (ObjectUtils.isNotNull(customerCreditMemoItemQty)) {
128             inputKey = ArConstants.CustomerCreditMemoConstants.CUSTOMER_CREDIT_MEMO_ITEM_QUANTITY;
129         }
130         else if (ObjectUtils.isNotNull(customerCreditMemoItemAmount)) {
131             inputKey = ArConstants.CustomerCreditMemoConstants.CUSTOMER_CREDIT_MEMO_ITEM_TOTAL_AMOUNT;
132         }
133 
134         return inputKey;
135     }
136 
137     public boolean isValueGreaterThanZero(BigDecimal value) {
138         boolean validValue = (value.compareTo(BigDecimal.ZERO) == 1 ? true : false);
139         if (!validValue) {
140             GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_QUANTITY, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_ITEM_QUANTITY_LESS_THAN_OR_EQUAL_TO_ZERO);
141         }
142         return validValue;
143     }
144 
145     public boolean isValueGreaterThanZero(KualiDecimal value) {
146         boolean validValue = value.isPositive();
147         if (!validValue) {
148             GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_TOTAL_AMOUNT, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_ITEM_AMOUNT_LESS_THAN_OR_EQUAL_TO_ZERO);
149         }
150         return validValue;
151     }
152 
153     public boolean isCustomerCreditMemoItemAmountLessThanEqualToInvoiceOpenItemAmount(CustomerCreditMemoDocument customerCreditMemoDocument, CustomerCreditMemoDetail customerCreditMemoDetail) {
154 
155         KualiDecimal invoiceOpenItemAmount = customerCreditMemoDetail.getInvoiceOpenItemAmount();
156         KualiDecimal creditMemoItemAmount = customerCreditMemoDetail.getCreditMemoItemTotalAmount();
157 
158         boolean validItemAmount = creditMemoItemAmount.isLessEqual(invoiceOpenItemAmount);
159         if (!validItemAmount) {
160             GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_TOTAL_AMOUNT, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_ITEM_AMOUNT_GREATER_THAN_INVOICE_ITEM_AMOUNT);
161         }
162 
163         return validItemAmount;
164     }
165 
166     public boolean isCustomerCreditMemoQtyLessThanEqualToInvoiceOpenQty(CustomerCreditMemoDetail customerCreditMemoDetail) {
167         BigDecimal invoiceOpenItemQty = customerCreditMemoDetail.getInvoiceOpenItemQuantity();
168         BigDecimal customerCreditMemoItemQty = customerCreditMemoDetail.getCreditMemoItemQuantity();
169 
170         // customer credit memo quantity must not be greater than invoice open item quantity
171         boolean validQuantity = (customerCreditMemoItemQty.compareTo(invoiceOpenItemQty) < 1 ? true : false);
172         if (!validQuantity) {
173             GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_QUANTITY, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_ITEM_QUANTITY_GREATER_THAN_INVOICE_ITEM_QUANTITY);
174         }
175 
176         return validQuantity;
177     }
178 
179     public boolean checkIfCustomerCreditMemoQtyAndCustomerCreditMemoItemAmountValid(CustomerCreditMemoDetail customerCreditMemoDetail, BigDecimal unitPrice) {
180         KualiDecimal creditAmount = customerCreditMemoDetail.getCreditMemoItemTotalAmount();
181         BigDecimal creditQuantity = customerCreditMemoDetail.getCreditMemoItemQuantity();
182 
183         // if unit price is zero, leave this validation, as it will cause an exception below by attempting to divide by zero
184         if (unitPrice.compareTo(BigDecimal.ZERO) == 0) {
185             // no need to report error, because it is already recorded by another validation check.
186             return false;
187         }
188 
189         // determine the expected exact total credit memo quantity, based on actual credit amount entered
190         BigDecimal expectedCreditQuantity = creditAmount.bigDecimalValue().divide(unitPrice, ArConstants.ITEM_QUANTITY_SCALE, BigDecimal.ROUND_HALF_UP);
191 
192         // return false if the expected quantity is 0 while the actual quantity is not
193         if (expectedCreditQuantity.compareTo(BigDecimal.ZERO) == 0 && creditQuantity.compareTo(BigDecimal.ZERO) != 0) {
194             return false;
195         }
196 
197         // determine the deviation percentage that the actual creditQuantity has from expectedCreditQuantity
198         BigDecimal deviationPercentage = creditQuantity.subtract(expectedCreditQuantity).divide(expectedCreditQuantity, ArConstants.ITEM_QUANTITY_SCALE, BigDecimal.ROUND_HALF_UP).abs();
199 
200         // only allow a certain deviation of creditQuantity from the expectedCreditQuantity
201         boolean validFlag = deviationPercentage.compareTo(ALLOWED_QTY_DEVIATION) < 1;
202 
203         if (!validFlag) {
204             GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_QUANTITY, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_INVALID_DATA_INPUT);
205             GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_ITEM_TOTAL_AMOUNT, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DETAIL_INVALID_DATA_INPUT);
206         }
207         return validFlag;
208     }
209 
210     /**
211      * @see org.kuali.kfs.module.ar.document.validation.RecalculateCustomerCreditMemoDocumentRule#processRecalculateCustomerCreditMemoDocumentRules(org.kuali.kfs.sys.document.AccountingDocument)
212      */
213     @Override
214     public boolean processRecalculateCustomerCreditMemoDocumentRules(TransactionalDocument document, boolean printErrMsgFlag) {
215         boolean success = true;
216         boolean crmDataEnteredFlag = false;
217         CustomerCreditMemoDocument customerCreditMemoDocument = (CustomerCreditMemoDocument) document;
218         List<CustomerCreditMemoDetail> customerCreditMemoDetails = customerCreditMemoDocument.getCreditMemoDetails();
219         int i = 0;
220         String propertyName;
221 
222         for (CustomerCreditMemoDetail customerCreditMemoDetail : customerCreditMemoDetails) {
223             propertyName = KFSConstants.CUSTOMER_CREDIT_MEMO_DETAIL_PROPERTY_NAME + "[" + i + "]";
224             GlobalVariables.getMessageMap().addToErrorPath(propertyName);
225 
226             // validate only if there is input data
227             if (!isQtyOrItemAmountEntered(customerCreditMemoDetail).equals(StringUtils.EMPTY)) {
228                 crmDataEnteredFlag = true;
229                 success &= processRecalculateCustomerCreditMemoDetailRules(customerCreditMemoDocument, customerCreditMemoDetail);
230             }
231             GlobalVariables.getMessageMap().removeFromErrorPath(propertyName);
232             i++;
233         }
234 
235         success &= crmDataEnteredFlag;
236 
237         // print error message if 'Submit'/'Save'/'Blanket Approved' button is pressed and there is no CRM data entered
238         if (!crmDataEnteredFlag && printErrMsgFlag) {
239             GlobalVariables.getMessageMap().putError(KFSConstants.DOCUMENT_PROPERTY_NAME, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_NO_DATA_TO_SUBMIT);
240         }
241 
242         return success;
243     }
244 
245     /**
246      * @see org.kuali.kfs.module.ar.document.validation.ContinueCustomerCreditMemoDocumentRule#processContinueCustomerCreditMemoDocumentRules(org.kuali.kfs.sys.document.AccountingDocument)
247      */
248     @Override
249     public boolean processContinueCustomerCreditMemoDocumentRules(TransactionalDocument document) {
250         boolean success;
251         CustomerCreditMemoDocument customerCreditMemoDocument = (CustomerCreditMemoDocument) document;
252 
253         success = checkIfInvoiceNumberIsFinal(customerCreditMemoDocument.getFinancialDocumentReferenceInvoiceNumber());
254         if (success) {
255             success = checkIfThereIsNoAnotherCRMInRouteForTheInvoice(customerCreditMemoDocument.getFinancialDocumentReferenceInvoiceNumber());
256         }
257         if (success) {
258             success = checkInvoiceForErrorCorrection(customerCreditMemoDocument.getFinancialDocumentReferenceInvoiceNumber());
259         }
260 
261         return success;
262     }
263 
264     public boolean checkIfInvoiceNumberIsFinal(String invDocumentNumber) {
265         boolean success = true;
266 
267         if (StringUtils.isBlank(invDocumentNumber)) {
268             success &= false;
269             GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT__INVOICE_DOCUMENT_NUMBER_IS_REQUIRED);
270         }
271         else {
272             CustomerInvoiceDocumentService service = SpringContext.getBean(CustomerInvoiceDocumentService.class);
273             CustomerInvoiceDocument customerInvoiceDocument = service.getInvoiceByInvoiceDocumentNumber(invDocumentNumber);
274             if (ObjectUtils.isNull(customerInvoiceDocument)) {
275                 success &= false;
276                 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_INVALID_INVOICE_DOCUMENT_NUMBER);
277             }
278             else if (!SpringContext.getBean(CustomerInvoiceDocumentService.class).checkIfInvoiceNumberIsFinal(invDocumentNumber)) {
279                 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_INVOICE_DOCUMENT_NOT_FINAL);
280                 success &= false;
281             }
282         }
283         return success;
284     }
285 
286     /**
287      * 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:
288      * processed, cancelled, or disapproved
289      *
290      * @param invoice
291      * @return
292      */
293     public boolean checkIfThereIsNoAnotherCRMInRouteForTheInvoice(String invoiceDocumentNumber) {
294 
295         WorkflowDocument workflowDocument;
296         boolean success = true;
297 
298         Map<String, String> fieldValues = new HashMap<String, String>();
299         fieldValues.put("financialDocumentReferenceInvoiceNumber", invoiceDocumentNumber);
300 
301         BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class);
302         Collection<CustomerCreditMemoDocument> customerCreditMemoDocuments = businessObjectService.findMatching(CustomerCreditMemoDocument.class, fieldValues);
303 
304         // no CRMs associated with the invoice are found
305         if (customerCreditMemoDocuments.isEmpty()) {
306             return success;
307         }
308 
309         String userId = GlobalVariables.getUserSession().getPrincipalId();
310         for(CustomerCreditMemoDocument customerCreditMemoDocument : customerCreditMemoDocuments) {
311             workflowDocument = WorkflowDocumentFactory.loadDocument(userId, customerCreditMemoDocument.getDocumentNumber());
312             if (!(workflowDocument.isApproved() || workflowDocument.isProcessed() || workflowDocument.isCanceled() || workflowDocument.isDisapproved())) {
313                 GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_ONE_CRM_IN_ROUTE_PER_INVOICE);
314                 success = false;
315                 break;
316             }
317         }
318         return success;
319     }
320 
321     /**
322      * This method checks if the Invoice has been error corrected or is an error correcting invoice
323      *
324      * @param invoice
325      * @return
326      */
327     public boolean checkInvoiceForErrorCorrection(String invoiceDocumentNumber) {
328         CustomerInvoiceDocumentService service = SpringContext.getBean(CustomerInvoiceDocumentService.class);
329         CustomerInvoiceDocument customerInvoiceDocument = service.getInvoiceByInvoiceDocumentNumber(invoiceDocumentNumber);
330 
331         // invoice has been corrected
332         if (customerInvoiceDocument.hasInvoiceBeenCorrected()) {
333             GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_CORRECTED_INVOICE);
334             return false;
335         }
336         // this is a correcting invoice
337         if (customerInvoiceDocument.isInvoiceReversal()) {
338             GlobalVariables.getMessageMap().putError(ArPropertyConstants.CustomerCreditMemoDocumentFields.CREDIT_MEMO_DOCUMENT_REF_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_CREDIT_MEMO_DOCUMENT_CORRECTING_INVOICE);
339             return false;
340         }
341         return true;
342     }
343 
344     @Override
345     public boolean isDocumentAttributesValid(Document document, boolean validateRequired) {
346         //refresh GLPE nonupdateable business object references....
347         CustomerCreditMemoDocument customerCreditMemoDocument = (CustomerCreditMemoDocument) document;
348 
349         for (CustomerCreditMemoDetail customerDetail : customerCreditMemoDocument.getCreditMemoDetails()) {
350             customerDetail.getCustomerInvoiceDetail().refreshNonUpdateableReferences();
351         }
352 
353         for (GeneralLedgerPendingEntry glpe : customerCreditMemoDocument.getGeneralLedgerPendingEntries()) {
354             glpe.refreshNonUpdateableReferences();
355         }
356 
357         return super.isDocumentAttributesValid(document, validateRequired);
358     }
359 
360 }