001/*
002 * Copyright 2009 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 */
016package org.kuali.ole.module.purap.document.validation.impl;
017
018import org.kuali.ole.module.purap.PurapConstants;
019import org.kuali.ole.module.purap.PurapConstants.ItemFields;
020import org.kuali.ole.module.purap.PurapKeyConstants;
021import org.kuali.ole.module.purap.PurapParameterConstants;
022import org.kuali.ole.module.purap.businessobject.InvoiceItem;
023import org.kuali.ole.module.purap.businessobject.PurApAccountingLine;
024import org.kuali.ole.module.purap.businessobject.PurApItem;
025import org.kuali.ole.module.purap.document.InvoiceDocument;
026import org.kuali.ole.module.purap.document.service.PurapService;
027import org.kuali.ole.select.OleSelectConstant;
028import org.kuali.ole.select.businessobject.OleInvoiceItem;
029import org.kuali.ole.select.document.OleInvoiceDocument;
030import org.kuali.ole.sys.OLEConstants;
031import org.kuali.ole.sys.OLEKeyConstants;
032import org.kuali.ole.sys.context.SpringContext;
033import org.kuali.ole.sys.document.validation.GenericValidation;
034import org.kuali.ole.sys.document.validation.Validation;
035import org.kuali.ole.sys.document.validation.event.AttributedDocumentEvent;
036import org.kuali.ole.sys.document.validation.impl.*;
037import org.kuali.ole.sys.service.impl.OleParameterConstants;
038import org.kuali.ole.vnd.businessobject.VendorCustomerNumber;
039import org.kuali.rice.core.api.util.type.KualiDecimal;
040import org.kuali.rice.coreservice.framework.parameter.ParameterService;
041import org.kuali.rice.krad.service.KRADServiceLocator;
042import org.kuali.rice.krad.util.GlobalVariables;
043import org.kuali.rice.krad.util.KRADConstants;
044import org.kuali.rice.krad.util.ObjectUtils;
045
046import java.math.BigDecimal;
047import java.util.ArrayList;
048import java.util.HashMap;
049import java.util.List;
050import java.util.Map;
051
052public class InvoiceProcessItemValidation extends GenericValidation {
053
054    private PurapService purapService;
055    private PurApItem itemForValidation;
056    private AttributedDocumentEvent event;
057    private CompositeValidation reviewAccountingLineValidation;
058    private InvoiceDocument invoiceDocument;
059    private PurApAccountingLine invoiceAccountingLine;
060
061    @Override
062    public boolean validate(AttributedDocumentEvent event) {
063        boolean valid = true;
064        this.event = event;
065
066        InvoiceDocument invoiceDocument = (InvoiceDocument) event.getDocument();
067        InvoiceItem invoiceItem = (InvoiceItem) itemForValidation;
068
069        valid &= validateEachItem(invoiceDocument, invoiceItem);
070
071        return valid;
072
073    }
074
075    /**
076     * Calls another validate item method and passes an identifier string from the item.
077     *
078     * @param invoiceDocument - payment request document.
079     * @param item
080     * @return
081     */
082    protected boolean validateEachItem(InvoiceDocument invoiceDocument, InvoiceItem item) {
083        boolean valid = true;
084        String identifierString = item.getItemIdentifierString();
085        valid &= validateItem(invoiceDocument, item, identifierString);
086        return valid;
087    }
088
089    /**
090     * Performs validation if full document entry not completed and peforms varying item validation.
091     * Such as, above the line, items without accounts, items with accounts.
092     *
093     * @param invoiceDocument  - payment request document
094     * @param item             - payment request item
095     * @param identifierString - identifier string used to mark in an error map
096     * @return
097     */
098    public boolean validateItem(InvoiceDocument invoiceDocument, InvoiceItem item, String identifierString) {
099        boolean valid = true;
100        // only run item validations if before full entry
101        if (!purapService.isFullDocumentEntryCompleted(invoiceDocument)) {
102            if (item.getItemType().isLineItemIndicator()) {
103                valid &= validateAboveTheLineItems(item, identifierString, invoiceDocument.isReceivingDocumentRequiredIndicator());
104                valid &= validatePOItem((OleInvoiceItem)item);
105            }
106            valid &= validateItemWithoutAccounts(item, identifierString);
107        }
108        // always run account validations
109        valid &= validateProrationType(invoiceDocument);
110        valid &= validateItemAccounts(invoiceDocument, item, identifierString);
111        return valid;
112    }
113
114    private boolean validatePOItem (OleInvoiceItem invoiceItem) {
115        if (invoiceItem.getItemType().isQuantityBasedGeneralLedgerIndicator()) {
116            if(invoiceItem.getPoItemIdentifier() == null ||
117                    invoiceItem.getPoItemIdentifier().compareTo(new Integer(0)) == 0) {
118                GlobalVariables.getMessageMap().putError(KRADConstants.GLOBAL_ERRORS, OLEConstants.ITEM_WITHOUT_PO);
119                return false;
120            }
121        }
122        return true;
123
124    }
125
126    protected boolean validateProrationType(InvoiceDocument invoiceDocument) {
127        boolean isValid = true;
128        OleInvoiceDocument document = (OleInvoiceDocument) invoiceDocument;
129        List<OleInvoiceItem> items = document.getItems();
130        boolean additionalItemPresent = false;
131        boolean canProrate = false;
132        KualiDecimal additionalCharge = KualiDecimal.ZERO;
133        KualiDecimal totalAmt = document.getInvoicedItemTotal() != null ?
134                new KualiDecimal(document.getInvoicedItemTotal()) : KualiDecimal.ZERO;
135        for (OleInvoiceItem invoiceItem : items) {
136            if (invoiceItem.getItemType().isAdditionalChargeIndicator() && invoiceItem.getExtendedPrice() != null &&
137                    !invoiceItem.getExtendedPrice().isZero()) {
138                additionalCharge = additionalCharge.add(invoiceItem.getExtendedPrice());
139                additionalItemPresent  = true;
140            }
141            if (invoiceItem.getItemType().isQuantityBasedGeneralLedgerIndicator() && invoiceItem.getItemUnitPrice().compareTo(BigDecimal.ZERO) != 0 ) {
142                canProrate = true;
143            }
144        }
145        if (additionalItemPresent && ((document.getProrateBy() == null) ||
146                (!document.isProrateDollar() && !document.isProrateManual() && !document.isProrateQty() && !document.isNoProrate()))) {
147            GlobalVariables.getMessageMap().putErrorForSectionId(OleSelectConstant.INVOICE_ADDITIONAL_ITEM_SECTION_ID,
148                    OLEKeyConstants.ERROR_REQUIRED, PurapConstants.PRQSDocumentsStrings.PRORATION_TYPE);
149            isValid &= false;
150
151        }
152        if ((totalAmt.isZero() || !canProrate) && document.isProrateDollar() ) {
153            GlobalVariables.getMessageMap().putError(OleSelectConstant.INVOICE_ADDITIONAL_CHARGE_SECTION_ID,
154                    OLEKeyConstants.ERROR_PRORATE_DOLLAR_ZERO_ITEM_TOTAL);
155        }
156        if (document.getVendorCustomerNumber() != null && !document.getVendorCustomerNumber().equalsIgnoreCase("")) {
157            Map<String, String> map = new HashMap<String, String>();
158            if (document.getVendorCustomerNumber() != null && !document.getVendorCustomerNumber().equalsIgnoreCase("")) {
159                map.put(OLEConstants.VENDOR_CUSTOMER_NUMBER, document.getVendorCustomerNumber());
160            }
161            if (document.getVendorHeaderGeneratedIdentifier() != null && !document.getVendorHeaderGeneratedIdentifier().toString().equalsIgnoreCase("")) {
162                map.put(OLEConstants.VENDOR_HEADER_IDENTIFIER, document.getVendorHeaderGeneratedIdentifier().toString());
163            }
164            if (document.getVendorDetailAssignedIdentifier() != null && !document.getVendorDetailAssignedIdentifier().toString().equalsIgnoreCase("")) {
165                map.put(OLEConstants.VENDOR_DETAIL_IDENTIFIER, document.getVendorDetailAssignedIdentifier().toString());
166            }
167            List<VendorCustomerNumber> vendorCustomerNumbers = (List<VendorCustomerNumber>) KRADServiceLocator.getBusinessObjectService().findMatching(VendorCustomerNumber.class, map);
168            if (!(vendorCustomerNumbers != null && vendorCustomerNumbers.size() > 0)) {
169                isValid &= false;
170                GlobalVariables.getMessageMap().putError(KRADConstants.GLOBAL_ERRORS, OLEConstants.INVALID_ACQUISITION_NUMBER);
171            }
172        }
173       /* if (totalAmt != null && totalAmt.isZero() && !additionalCharge.isZero() && document.isProrateDollar() ) {
174            GlobalVariables.getMessageMap().putError(OleSelectConstant.INVOICE_ADDITIONAL_CHARGE_SECTION_ID,
175                    OLEKeyConstants.ERROR_PRORATE_DOLLAR_ZERO_ITEM_TOTAL);
176            isValid &= false;
177        }*/
178        return isValid;
179    }
180    /**
181     * Validates above the line items.
182     *
183     * @param item             - payment request item
184     * @param identifierString - identifier string used to mark in an error map
185     * @return
186     */
187    protected boolean validateAboveTheLineItems(InvoiceItem item, String identifierString, boolean isReceivingDocumentRequiredIndicator) {
188        boolean valid = true;
189        // Currently Quantity is allowed to be NULL on screen;
190        // must be either a positive number or NULL for DB
191        if (ObjectUtils.isNotNull(item.getItemQuantity())) {
192            /*if (item.getItemQuantity().isNegative()) {
193                // if quantity is negative give an error
194                valid = false;
195                GlobalVariables.getMessageMap().putError(PurapConstants.ITEM_TAB_ERROR_PROPERTY, PurapKeyConstants.ERROR_ITEM_AMOUNT_BELOW_ZERO, ItemFields.INVOICE_QUANTITY, identifierString);
196            }*/
197            if (!isReceivingDocumentRequiredIndicator) {
198                /*if (item.getPoOutstandingQuantity().isLessThan(item.getItemQuantity())) {
199                    valid = false;
200                    GlobalVariables.getMessageMap().putError(PurapConstants.ITEM_TAB_ERROR_PROPERTY, PurapKeyConstants.ERROR_ITEM_QUANTITY_TOO_MANY, ItemFields.INVOICE_QUANTITY, identifierString, ItemFields.OPEN_QUANTITY);
201                } */
202            }
203        }
204        // if (ObjectUtils.isNotNull(item.getExtendedPrice()) && item.getExtendedPrice().isPositive() && ObjectUtils.isNotNull(item.getPoOutstandingQuantity()) && item.getPoOutstandingQuantity().isPositive()) {
205        if (ObjectUtils.isNotNull(item.getExtendedPrice()) && item.getExtendedPrice().isPositive()) {
206
207            // here we must require the user to enter some value for quantity if they want a credit amount associated
208            if (ObjectUtils.isNull(item.getItemQuantity()) || item.getItemQuantity().isZero()) {
209                // here we have a user not entering a quantity with an extended amount but the PO has a quantity...require user to
210                // enter a quantity
211                valid = false;
212                GlobalVariables.getMessageMap().putError(PurapConstants.ITEM_TAB_ERROR_PROPERTY, PurapKeyConstants.ERROR_ITEM_QUANTITY_REQUIRED, ItemFields.INVOICE_QUANTITY, identifierString, ItemFields.OPEN_QUANTITY);
213            }
214        }
215
216        // check that non-quantity based items are not trying to pay on a zero encumbrance amount (check only prior to ap approval)
217        if ((ObjectUtils.isNull(item.getInvoice().getPurapDocumentIdentifier())) || (PurapConstants.InvoiceStatuses.APPDOC_IN_PROCESS.equals(item.getInvoice().getApplicationDocumentStatus()))) {
218// RICE20 : needed? :  !purapService.isFullDocumentEntryCompleted(item.getInvoice())) {
219            if ((item.getItemType().isAmountBasedGeneralLedgerIndicator()) && ((item.getExtendedPrice() != null) && item.getExtendedPrice().isNonZero())) {
220                if (item.getPoOutstandingAmount() == null || item.getPoOutstandingAmount().isZero()) {
221                    valid = false;
222                    GlobalVariables.getMessageMap().putError(PurapConstants.ITEM_TAB_ERROR_PROPERTY, PurapKeyConstants.ERROR_ITEM_AMOUNT_ALREADY_PAID, identifierString);
223                }
224            }
225        }
226
227        return valid;
228    }
229
230    /**
231     * Validates that the item must contain at least one account
232     *
233     * @param item - payment request item
234     * @return
235     */
236    public boolean validateItemWithoutAccounts(InvoiceItem item, String identifierString) {
237        boolean valid = true;
238        if (ObjectUtils.isNotNull(item.getItemUnitPrice()) && (new KualiDecimal(item.getItemUnitPrice())).isNonZero() && item.isAccountListEmpty()) {
239            valid = false;
240            GlobalVariables.getMessageMap().putError(PurapConstants.ITEM_TAB_ERROR_PROPERTY, PurapKeyConstants.ERROR_ITEM_ACCOUNTING_INCOMPLETE, identifierString);
241        }
242        return valid;
243    }
244
245    /**
246     * Validates the totals for the item by account, that the total by each accounting line for the item, matches
247     * the extended price on the item.
248     *
249     * @param invoiceDocument  - payment request document
250     * @param item             - payment request item to validate
251     * @param identifierString - identifier string used to mark in an error map
252     * @return
253     */
254    public boolean validateItemAccounts(InvoiceDocument invoiceDocument, InvoiceItem item, String identifierString) {
255        boolean valid = true;
256        List<PurApAccountingLine> accountingLines = item.getSourceAccountingLines();
257        KualiDecimal itemTotal = item.getTotalAmount();
258        KualiDecimal accountTotal = KualiDecimal.ZERO;
259        KualiDecimal prorateSurcharge = KualiDecimal.ZERO;
260        OleInvoiceItem invoiceItem = (OleInvoiceItem) item;
261        if (invoiceItem.getItemType().isQuantityBasedGeneralLedgerIndicator() && invoiceItem.getExtendedPrice() != null && invoiceItem.getExtendedPrice().compareTo(KualiDecimal.ZERO) != 0) {
262            if (invoiceItem.getItemSurcharge() != null && invoiceItem.getItemTypeCode().equals("ITEM")) {
263                prorateSurcharge = new KualiDecimal(invoiceItem.getItemSurcharge()).multiply(invoiceItem.getItemQuantity());
264            }
265            itemTotal = itemTotal.subtract(prorateSurcharge);
266        }
267        for (PurApAccountingLine accountingLine : accountingLines) {
268            if (accountingLine.getAmount().isZero()) {
269                if (!canApproveAccountingLinesWithZeroAmount()) {
270                    GlobalVariables.getMessageMap().putError(PurapConstants.ITEM_TAB_ERROR_PROPERTY, PurapKeyConstants.ERROR_ITEM_ACCOUNTING_AMOUNT_INVALID, itemForValidation.getItemIdentifierString());
271                    valid &= false;
272                }
273            }
274            valid &= reviewAccountingLineValidation(invoiceDocument, accountingLine);
275            accountTotal = accountTotal.add(accountingLine.getAmount());
276        }
277        if (purapService.isFullDocumentEntryCompleted(invoiceDocument)) {
278            // check amounts not percent after full entry
279            if (accountTotal.compareTo(itemTotal) != 0) {
280                valid = false;
281                GlobalVariables.getMessageMap().putError(PurapConstants.ITEM_TAB_ERROR_PROPERTY, PurapKeyConstants.ERROR_ITEM_ACCOUNTING_AMOUNT_TOTAL, identifierString);
282            }
283        }
284        return valid;
285    }
286
287    public CompositeValidation getReviewAccountingLineValidation() {
288        return reviewAccountingLineValidation;
289    }
290
291    public void setReviewAccountingLineValidation(CompositeValidation reviewAccountingLineValidation) {
292        this.reviewAccountingLineValidation = reviewAccountingLineValidation;
293    }
294
295    public PurapService getPurapService() {
296        return purapService;
297    }
298
299    public void setPurapService(PurapService purapService) {
300        this.purapService = purapService;
301    }
302
303    public PurApItem getItemForValidation() {
304        return itemForValidation;
305    }
306
307    public void setItemForValidation(PurApItem itemForValidation) {
308        this.itemForValidation = itemForValidation;
309    }
310
311    protected boolean reviewAccountingLineValidation(InvoiceDocument document, PurApAccountingLine accountingLine) {
312        boolean valid = true;
313        List<Validation> gauntlet = new ArrayList<Validation>();
314        this.invoiceDocument = document;
315        this.invoiceAccountingLine = accountingLine;
316
317        createGauntlet(reviewAccountingLineValidation);
318
319        for (Validation validation : gauntlet) {
320            valid &= validation.validate(event);
321        }
322
323        return valid;
324    }
325
326    protected void createGauntlet(CompositeValidation validation) {
327        for (Validation val : validation.getValidations()) {
328            if (val instanceof CompositeValidation) {
329                createGauntlet((CompositeValidation) val);
330            } else if (val instanceof BusinessObjectDataDictionaryValidation) {
331                addParametersToValidation((BusinessObjectDataDictionaryValidation) val);
332            } else if (val instanceof AccountingLineAmountPositiveValidation) {
333                //addParametersToValidation((AccountingLineAmountPositiveValidation) val);
334            } else if (val instanceof AccountingLineDataDictionaryValidation) {
335                addParametersToValidation((AccountingLineDataDictionaryValidation) val);
336            } else if (val instanceof AccountingLineValuesAllowedValidationHutch) {
337                addParametersToValidation((AccountingLineValuesAllowedValidationHutch) val);
338            } else {
339                throw new IllegalStateException("Validations in the InvoiceProcessItemValidation must contain specific instances of validation");
340            }
341        }
342    }
343
344    /**
345     * checks if an accounting line with zero dollar amount can be approved.  This will check
346     * the system parameter APPROVE_ACCOUNTING_LINES_WITH_ZERO_DOLLAR_AMOUNT_IND and determines if the
347     * line can be approved or not.
348     *
349     * @return true if the system parameter value is Y else returns N.
350     */
351    public boolean canApproveAccountingLinesWithZeroAmount() {
352        boolean canApproveLine = false;
353
354        // get parameter to see if accounting line with zero dollar amount can be approved.
355        String approveZeroAmountLine = SpringContext.getBean(ParameterService.class).getParameterValueAsString(OleParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.APPROVE_ACCOUNTING_LINES_WITH_ZERO_DOLLAR_AMOUNT_IND);
356
357        if ("Y".equalsIgnoreCase(approveZeroAmountLine)) {
358            return true;
359        }
360
361        return canApproveLine;
362    }
363
364    protected void addParametersToValidation(BusinessObjectDataDictionaryValidation validation) {
365        validation.setBusinessObjectForValidation(this.invoiceAccountingLine);
366    }
367
368    protected void addParametersToValidation(AccountingLineAmountPositiveValidation validation) {
369        validation.setAccountingDocumentForValidation(this.invoiceDocument);
370        validation.setAccountingLineForValidation(this.invoiceAccountingLine);
371    }
372
373    protected void addParametersToValidation(AccountingLineDataDictionaryValidation validation) {
374        validation.setAccountingLineForValidation(this.invoiceAccountingLine);
375    }
376
377    protected void addParametersToValidation(AccountingLineValuesAllowedValidationHutch validation) {
378        validation.setAccountingDocumentForValidation(this.invoiceDocument);
379        validation.setAccountingLineForValidation(this.invoiceAccountingLine);
380    }
381
382    /**
383     * Gets the event attribute.
384     *
385     * @return Returns the event.
386     */
387    protected AttributedDocumentEvent getEvent() {
388        return event;
389    }
390
391    /**
392     * Sets the event attribute value.
393     *
394     * @param event The event to set.
395     */
396    protected void setEvent(AttributedDocumentEvent event) {
397        this.event = event;
398    }
399
400    /**
401     * Gets the invoiceDocument attribute.
402     *
403     * @return Returns the invoiceDocument.
404     */
405    protected InvoiceDocument getPreqDocument() {
406        return invoiceDocument;
407    }
408
409    /**
410     * Sets the invoiceDocument attribute value.
411     *
412     * @param invoiceDocument The invoiceDocument to set.
413     */
414    protected void setPreqDocument(InvoiceDocument invoiceDocument) {
415        this.invoiceDocument = invoiceDocument;
416    }
417
418    /**
419     * Gets the invoiceAccountingLine attribute.
420     *
421     * @return Returns the invoiceAccountingLine.
422     */
423    protected PurApAccountingLine getPreqAccountingLine() {
424        return invoiceAccountingLine;
425    }
426
427    /**
428     * Sets the invoiceAccountingLine attribute value.
429     *
430     * @param invoiceAccountingLine The invoiceAccountingLine to set.
431     */
432    protected void setPreqAccountingLine(PurApAccountingLine invoiceAccountingLine) {
433        this.invoiceAccountingLine = invoiceAccountingLine;
434    }
435
436}