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    }