0001: /*
0002: * Copyright 2006-2007 The Kuali Foundation.
0003: *
0004: * Licensed under the Educational Community License, Version 1.0 (the "License");
0005: * you may not use this file except in compliance with the License.
0006: * You may obtain a copy of the License at
0007: *
0008: * http://www.opensource.org/licenses/ecl1.php
0009: *
0010: * Unless required by applicable law or agreed to in writing, software
0011: * distributed under the License is distributed on an "AS IS" BASIS,
0012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0013: * See the License for the specific language governing permissions and
0014: * limitations under the License.
0015: */
0016: package org.kuali.module.purap.rules;
0017:
0018: import java.math.BigDecimal;
0019: import java.util.ArrayList;
0020: import java.util.Collection;
0021: import java.util.Date;
0022: import java.util.Iterator;
0023: import java.util.List;
0024:
0025: import org.apache.commons.lang.StringUtils;
0026: import org.kuali.RicePropertyConstants;
0027: import org.kuali.core.document.Document;
0028: import org.kuali.core.util.GlobalVariables;
0029: import org.kuali.core.util.KualiDecimal;
0030: import org.kuali.core.util.ObjectUtils;
0031: import org.kuali.kfs.KFSConstants;
0032: import org.kuali.kfs.KFSKeyConstants;
0033: import org.kuali.kfs.bo.AccountingLine;
0034: import org.kuali.kfs.bo.GeneralLedgerPendingEntry;
0035: import org.kuali.kfs.bo.SourceAccountingLine;
0036: import org.kuali.kfs.context.SpringContext;
0037: import org.kuali.kfs.document.AccountingDocument;
0038: import org.kuali.module.financial.service.UniversityDateService;
0039: import org.kuali.module.purap.PurapConstants;
0040: import org.kuali.module.purap.PurapKeyConstants;
0041: import org.kuali.module.purap.PurapPropertyConstants;
0042: import org.kuali.module.purap.PurapConstants.ItemFields;
0043: import org.kuali.module.purap.PurapConstants.PREQDocumentsStrings;
0044: import org.kuali.module.purap.PurapConstants.PurapDocTypeCodes;
0045: import org.kuali.module.purap.bo.PaymentRequestItem;
0046: import org.kuali.module.purap.bo.PurApAccountingLine;
0047: import org.kuali.module.purap.bo.PurApItem;
0048: import org.kuali.module.purap.bo.PurchaseOrderItem;
0049: import org.kuali.module.purap.document.AccountsPayableDocument;
0050: import org.kuali.module.purap.document.PaymentRequestDocument;
0051: import org.kuali.module.purap.document.PurchaseOrderDocument;
0052: import org.kuali.module.purap.document.PurchasingAccountsPayableDocument;
0053: import org.kuali.module.purap.document.authorization.PaymentRequestDocumentActionAuthorizer;
0054: import org.kuali.module.purap.service.PaymentRequestService;
0055: import org.kuali.module.purap.service.PurapAccountingService;
0056: import org.kuali.module.purap.service.PurapGeneralLedgerService;
0057: import org.kuali.module.purap.service.PurapService;
0058:
0059: /**
0060: * Business rule(s) applicable to Payment Request documents.
0061: */
0062: public class PaymentRequestDocumentRule extends
0063: AccountsPayableDocumentRuleBase {
0064:
0065: private static KualiDecimal zero = new KualiDecimal(0);
0066: private static BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100);
0067:
0068: /**
0069: * Returns true if full document entry is complete, bypassing further rules.
0070: *
0071: * @see org.kuali.module.purap.rules.PurapAccountingDocumentRuleBase#processCustomAddAccountingLineBusinessRules(org.kuali.kfs.document.AccountingDocument, org.kuali.kfs.bo.AccountingLine)
0072: */
0073: @Override
0074: protected boolean processCustomAddAccountingLineBusinessRules(
0075: AccountingDocument financialDocument,
0076: AccountingLine accountingLine) {
0077: PaymentRequestDocument preq = (PaymentRequestDocument) financialDocument;
0078: if (SpringContext.getBean(PurapService.class)
0079: .isFullDocumentEntryCompleted(preq)) {
0080: return true;
0081: }
0082: return super .processCustomAddAccountingLineBusinessRules(
0083: financialDocument, accountingLine);
0084: }
0085:
0086: /**
0087: * Overriding the accessibility of the accounts only in the case where awaiting ap review, that is because the super
0088: * checks enroute and checks if it is the owner while we allow "full entry" until past this stage
0089: *
0090: * @see org.kuali.kfs.rules.AccountingDocumentRuleBase#accountIsAccessible(org.kuali.kfs.document.AccountingDocument,
0091: * org.kuali.kfs.bo.AccountingLine)
0092: */
0093: @Override
0094: protected boolean accountIsAccessible(
0095: AccountingDocument financialDocument,
0096: AccountingLine accountingLine) {
0097: PaymentRequestDocument preq = (PaymentRequestDocument) financialDocument;
0098: // We are overriding the accessibility of the accounts only in the case where awaiting ap review, that is because the super
0099: // checks enroute
0100: // and checks if it is the owner while we allow "full entry" until past this stage
0101: if (StringUtils
0102: .equals(
0103: PurapConstants.PaymentRequestStatuses.AWAITING_ACCOUNTS_PAYABLE_REVIEW,
0104: preq.getStatusCode())) {
0105: return true;
0106: } else {
0107: return super .accountIsAccessible(financialDocument,
0108: accountingLine);
0109: }
0110: }
0111:
0112: /**
0113: * Tabs included on Payment Request Documents are: Invoice
0114: *
0115: * @see org.kuali.module.purap.rules.PurchasingAccountsPayableDocumentRuleBase#processValidation(org.kuali.module.purap.document.PurchasingAccountsPayableDocument)
0116: */
0117: @Override
0118: public boolean processValidation(
0119: PurchasingAccountsPayableDocument purapDocument) {
0120: boolean valid = super .processValidation(purapDocument);
0121: PaymentRequestDocument preqDocument = (PaymentRequestDocument) purapDocument;
0122: valid &= processInvoiceValidation(preqDocument);
0123: if (!SpringContext.getBean(PurapService.class)
0124: .isFullDocumentEntryCompleted(purapDocument)) {
0125: valid &= processPurchaseOrderIDValidation(preqDocument);
0126: }
0127: valid &= processPaymentRequestDateValidationForContinue(preqDocument);
0128: valid &= processVendorValidation(preqDocument);
0129: valid &= validatePaymentRequestDates(preqDocument);
0130: return valid;
0131: }
0132:
0133: /**
0134: * @see org.kuali.core.rules.DocumentRuleBase#processCustomRouteDocumentBusinessRules(org.kuali.core.document.Document)
0135: */
0136: @Override
0137: protected boolean processCustomRouteDocumentBusinessRules(
0138: Document document) {
0139: boolean isValid = true;
0140: PaymentRequestDocument paymentRequestDocument = (PaymentRequestDocument) document;
0141: isValid &= validateRouteFiscal(paymentRequestDocument);
0142: isValid &= processValidation(paymentRequestDocument);
0143: isValid &= checkNegativeAccounts(paymentRequestDocument);
0144: return isValid;
0145: }
0146:
0147: /**
0148: * Ensures source accounting lines amounts are not negative.
0149: *
0150: * @param paymentRequestDocument - payment request document to validate
0151: * @return
0152: */
0153: private boolean checkNegativeAccounts(
0154: PaymentRequestDocument paymentRequestDocument) {
0155: boolean valid = true;
0156: GlobalVariables.getErrorMap().clearErrorPath();
0157: GlobalVariables.getErrorMap().addToErrorPath(
0158: RicePropertyConstants.DOCUMENT);
0159:
0160: // if this was set somewhere on the doc(for later use) in prepare for save we could avoid this call
0161: List<SourceAccountingLine> sourceLines = SpringContext.getBean(
0162: PurapAccountingService.class).generateSummary(
0163: paymentRequestDocument.getItems());
0164:
0165: for (SourceAccountingLine sourceAccountingLine : sourceLines) {
0166: if (sourceAccountingLine.getAmount().isNegative()) {
0167: String accountString = sourceAccountingLine
0168: .getChartOfAccountsCode()
0169: + " - "
0170: + sourceAccountingLine.getAccountNumber()
0171: + " - "
0172: + sourceAccountingLine.getFinancialObjectCode();
0173: GlobalVariables.getErrorMap().putError(
0174: PurapConstants.ITEM_TAB_ERROR_PROPERTY,
0175: PurapKeyConstants.ERROR_ACCOUNT_AMOUNT_TOTAL,
0176: accountString,
0177: sourceAccountingLine.getAmount() + "");
0178: valid &= false;
0179: }
0180: }
0181: GlobalVariables.getErrorMap().clearErrorPath();
0182: return valid;
0183: }
0184:
0185: /**
0186: * @see org.kuali.module.purap.rule.ContinueAccountsPayableRule#processContinueAccountsPayableBusinessRules(org.kuali.module.purap.document.AccountsPayableDocument)
0187: */
0188: public boolean processContinueAccountsPayableBusinessRules(
0189: AccountsPayableDocument apDocument) {
0190: boolean valid = true;
0191: PaymentRequestDocument paymentRequestDocument = (PaymentRequestDocument) apDocument;
0192: valid &= processPurchaseOrderIDValidation(paymentRequestDocument);
0193: valid &= processInvoiceValidation(paymentRequestDocument);
0194: valid &= processPaymentRequestDateValidationForContinue(paymentRequestDocument);
0195: return valid;
0196: }
0197:
0198: /**
0199: * @see org.kuali.module.purap.rule.CalculateAccountsPayableRule#processCalculateAccountsPayableBusinessRules(org.kuali.module.purap.document.AccountsPayableDocument)
0200: */
0201: public boolean processCalculateAccountsPayableBusinessRules(
0202: AccountsPayableDocument apDocument) {
0203: boolean valid = true;
0204: GlobalVariables.getErrorMap().clearErrorPath();
0205: GlobalVariables.getErrorMap().addToErrorPath(
0206: RicePropertyConstants.DOCUMENT);
0207: PaymentRequestDocument paymentRequestDocument = (PaymentRequestDocument) apDocument;
0208: // Give warnings for the following. The boolean results of the calls are not to be used here.
0209: boolean totalsMatch = validateTotals(paymentRequestDocument);
0210: boolean payDateOk = validatePayDateNotOverThresholdDaysAway(paymentRequestDocument);
0211: // The Grand Total Amount must be greate than zero.
0212: if (paymentRequestDocument.getGrandTotal().compareTo(
0213: KualiDecimal.ZERO) <= 0) {
0214: GlobalVariables
0215: .getErrorMap()
0216: .putError(
0217: PurapPropertyConstants.GRAND_TOTAL,
0218: PurapKeyConstants.ERROR_PAYMENT_REQUEST_GRAND_TOTAL_NOT_POSITIVE);
0219: valid &= false;
0220: }
0221: // The Payment Request Pay Date must not be in the past.
0222: valid &= validatePayDateNotPast(paymentRequestDocument);
0223: GlobalVariables.getErrorMap().clearErrorPath();
0224: return valid;
0225: }
0226:
0227: /**
0228: * Validates item fields are valid for the calculation process.
0229: *
0230: * @see org.kuali.module.purap.rule.PreCalculateAccountsPayableRule#processPreCalculateAccountsPayableBusinessRules(org.kuali.module.purap.document.AccountsPayableDocument)
0231: */
0232: public boolean processPreCalculateAccountsPayableBusinessRules(
0233: AccountsPayableDocument document) {
0234: boolean valid = true;
0235: PaymentRequestDocument preqDocument = (PaymentRequestDocument) document;
0236:
0237: return valid;
0238: }
0239:
0240: /**
0241: * Performs any validation for the Invoice tab.
0242: *
0243: * @param preqDocument - payment request document
0244: * @return
0245: */
0246: public boolean processInvoiceValidation(
0247: PaymentRequestDocument preqDocument) {
0248: boolean valid = true;
0249: GlobalVariables.getErrorMap().clearErrorPath();
0250: GlobalVariables.getErrorMap().addToErrorPath(
0251: RicePropertyConstants.DOCUMENT);
0252: if (ObjectUtils.isNull(preqDocument
0253: .getPurchaseOrderIdentifier())) {
0254: GlobalVariables.getErrorMap().putError(
0255: PurapPropertyConstants.PURCHASE_ORDER_IDENTIFIER,
0256: KFSKeyConstants.ERROR_REQUIRED,
0257: PREQDocumentsStrings.PURCHASE_ORDER_ID);
0258: valid &= false;
0259: }
0260: if (ObjectUtils.isNull(preqDocument.getInvoiceDate())) {
0261: GlobalVariables.getErrorMap().putError(
0262: PurapPropertyConstants.INVOICE_DATE,
0263: KFSKeyConstants.ERROR_REQUIRED,
0264: PREQDocumentsStrings.INVOICE_DATE);
0265: valid &= false;
0266: }
0267: if (StringUtils.isBlank(preqDocument.getInvoiceNumber())) {
0268: GlobalVariables.getErrorMap().putError(
0269: PurapPropertyConstants.INVOICE_NUMBER,
0270: KFSKeyConstants.ERROR_REQUIRED,
0271: PREQDocumentsStrings.INVOICE_NUMBER);
0272: valid &= false;
0273: }
0274: if (ObjectUtils.isNull(preqDocument.getVendorInvoiceAmount())) {
0275: GlobalVariables.getErrorMap().putError(
0276: PurapPropertyConstants.VENDOR_INVOICE_AMOUNT,
0277: KFSKeyConstants.ERROR_REQUIRED,
0278: PREQDocumentsStrings.VENDOR_INVOICE_AMOUNT);
0279: valid &= false;
0280: }
0281: GlobalVariables.getErrorMap().clearErrorPath();
0282: return valid;
0283: }
0284:
0285: /**
0286: * Performs validation on the vendor on the payment request document.
0287: *
0288: * @param preqDocument - payment request document.
0289: * @return
0290: */
0291: public boolean processVendorValidation(
0292: PaymentRequestDocument preqDocument) {
0293: boolean valid = true;
0294: GlobalVariables.getErrorMap().clearErrorPath();
0295: GlobalVariables.getErrorMap().addToErrorPath(
0296: RicePropertyConstants.DOCUMENT);
0297: if (StringUtils.equals(preqDocument.getVendorCountryCode(),
0298: KFSConstants.COUNTRY_CODE_UNITED_STATES)) {
0299: if (StringUtils.isBlank(preqDocument.getVendorStateCode())) {
0300: GlobalVariables.getErrorMap().putError(
0301: PurapPropertyConstants.VENDOR_STATE_CODE,
0302: KFSKeyConstants.ERROR_REQUIRED_FOR_US,
0303: PREQDocumentsStrings.VENDOR_STATE);
0304: valid &= false;
0305: }
0306: if (StringUtils.isBlank(preqDocument.getVendorPostalCode())) {
0307: GlobalVariables.getErrorMap().putError(
0308: PurapPropertyConstants.VENDOR_POSTAL_CODE,
0309: KFSKeyConstants.ERROR_REQUIRED_FOR_US,
0310: PREQDocumentsStrings.VENDOR_POSTAL_CODE);
0311: valid &= false;
0312: }
0313:
0314: }
0315: GlobalVariables.getErrorMap().clearErrorPath();
0316: return valid;
0317: }
0318:
0319: /**
0320: * Performs various validation on the purchase order.
0321: *
0322: * @param document
0323: * @return
0324: */
0325: protected boolean processPurchaseOrderIDValidation(
0326: PaymentRequestDocument document) {
0327: boolean valid = true;
0328: GlobalVariables.getErrorMap().clearErrorPath();
0329: GlobalVariables.getErrorMap().addToErrorPath(
0330: RicePropertyConstants.DOCUMENT);
0331:
0332: Integer POID = document.getPurchaseOrderIdentifier();
0333:
0334: PurchaseOrderDocument purchaseOrderDocument = document
0335: .getPurchaseOrderDocument();
0336: if (ObjectUtils.isNull(purchaseOrderDocument)) {
0337: GlobalVariables.getErrorMap().putError(
0338: PurapPropertyConstants.PURCHASE_ORDER_IDENTIFIER,
0339: PurapKeyConstants.ERROR_PURCHASE_ORDER_NOT_EXIST);
0340: valid &= false;
0341: } else if (purchaseOrderDocument.isPendingActionIndicator()) {
0342: GlobalVariables.getErrorMap().putError(
0343: PurapPropertyConstants.PURCHASE_ORDER_IDENTIFIER,
0344: PurapKeyConstants.ERROR_PURCHASE_PENDING_ACTION);
0345: valid &= false;
0346: } else if (!StringUtils.equals(purchaseOrderDocument
0347: .getStatusCode(),
0348: PurapConstants.PurchaseOrderStatuses.OPEN)) {
0349: GlobalVariables.getErrorMap().putError(
0350: PurapPropertyConstants.PURCHASE_ORDER_IDENTIFIER,
0351: PurapKeyConstants.ERROR_PURCHASE_ORDER_NOT_OPEN);
0352: valid &= false;
0353: // if the PO is pending and it is not a Retransmit, we cannot generate a Payment Request for it
0354: } else {
0355: // Verify that there exists at least 1 item left to be invoiced
0356: valid &= encumberedItemExistsForInvoicing(purchaseOrderDocument);
0357: }
0358: GlobalVariables.getErrorMap().clearErrorPath();
0359: return valid;
0360: }
0361:
0362: /**
0363: * Determines if there are items with encumbrances to be invoiced on passed in
0364: * purchase order document.
0365: *
0366: * @param document - purchase order document
0367: * @return
0368: */
0369: public boolean encumberedItemExistsForInvoicing(
0370: PurchaseOrderDocument document) {
0371: boolean zeroDollar = true;
0372: GlobalVariables.getErrorMap().clearErrorPath();
0373: GlobalVariables.getErrorMap().addToErrorPath(
0374: RicePropertyConstants.DOCUMENT);
0375: for (PurchaseOrderItem poi : (List<PurchaseOrderItem>) document
0376: .getItems()) {
0377: // Quantity-based items
0378: if (poi.getItemType().isItemTypeAboveTheLineIndicator()
0379: && poi.getItemType()
0380: .isQuantityBasedGeneralLedgerIndicator()) {
0381: KualiDecimal encumberedQuantity = poi
0382: .getItemOutstandingEncumberedQuantity() == null ? zero
0383: : poi.getItemOutstandingEncumberedQuantity();
0384: if (encumberedQuantity.compareTo(zero) == 1) {
0385: zeroDollar = false;
0386: break;
0387: }
0388: }
0389: // Service Items or Below-the-line Items
0390: else if (poi.getItemType()
0391: .isAmountBasedGeneralLedgerIndicator()
0392: || !poi.getItemType()
0393: .isItemTypeAboveTheLineIndicator()) {
0394: KualiDecimal encumberedAmount = poi
0395: .getItemOutstandingEncumberedAmount() == null ? zero
0396: : poi.getItemOutstandingEncumberedAmount();
0397: if (encumberedAmount.compareTo(zero) == 1) {
0398: zeroDollar = false;
0399: break;
0400: }
0401: }
0402: }
0403: if (zeroDollar) {
0404: GlobalVariables.getErrorMap().putError(
0405: PurapPropertyConstants.PURCHASE_ORDER_IDENTIFIER,
0406: PurapKeyConstants.ERROR_NO_ITEMS_TO_INVOICE);
0407: }
0408: GlobalVariables.getErrorMap().clearErrorPath();
0409: return !zeroDollar;
0410: }
0411:
0412: /**
0413: * Ensures invoice date does not occur in the future.
0414: *
0415: * @param document
0416: * @return
0417: */
0418: protected boolean processPaymentRequestDateValidationForContinue(
0419: PaymentRequestDocument document) {
0420: boolean valid = true;
0421: GlobalVariables.getErrorMap().clearErrorPath();
0422: GlobalVariables.getErrorMap().addToErrorPath(
0423: RicePropertyConstants.DOCUMENT);
0424: // invoice date validation
0425: java.sql.Date invoiceDate = document.getInvoiceDate();
0426: if (ObjectUtils.isNotNull(invoiceDate)
0427: && SpringContext.getBean(PaymentRequestService.class)
0428: .isInvoiceDateAfterToday(invoiceDate)) {
0429: GlobalVariables.getErrorMap().putError(
0430: PurapPropertyConstants.INVOICE_DATE,
0431: PurapKeyConstants.ERROR_INVALID_INVOICE_DATE);
0432: valid &= false;
0433: }
0434: GlobalVariables.getErrorMap().clearErrorPath();
0435: return valid;
0436: }
0437:
0438: /**
0439: * Validates payment request dates.
0440: *
0441: * @param document - payment request document
0442: * @return
0443: */
0444: protected boolean validatePaymentRequestDates(
0445: PaymentRequestDocument document) {
0446: boolean valid = true;
0447: GlobalVariables.getErrorMap().clearErrorPath();
0448: GlobalVariables.getErrorMap().addToErrorPath(
0449: RicePropertyConstants.DOCUMENT);
0450: // Pay date in the past validation.
0451: valid &= validatePayDateNotPast(document);
0452: GlobalVariables.getErrorMap().clearErrorPath();
0453: return valid;
0454: }
0455:
0456: /**
0457: * Validates that the payment request date does not occur in the past.
0458: *
0459: * @param document - payment request document
0460: * @return
0461: */
0462: boolean validatePayDateNotPast(PaymentRequestDocument document) {
0463: boolean valid = true;
0464: java.sql.Date paymentRequestPayDate = document
0465: .getPaymentRequestPayDate();
0466: if (ObjectUtils.isNotNull(paymentRequestPayDate)
0467: && SpringContext.getBean(PurapService.class)
0468: .isDateInPast(paymentRequestPayDate)) {
0469: valid &= false;
0470: GlobalVariables.getErrorMap().putError(
0471: PurapPropertyConstants.PAYMENT_REQUEST_PAY_DATE,
0472: PurapKeyConstants.ERROR_INVALID_PAY_DATE);
0473: }
0474: return valid;
0475: }
0476:
0477: /**
0478: * This method side-effects a warning, and consequently should not be used in such a way as to cause validation to fail. Returns
0479: * a boolean for ease of testing. If the threshold days value is positive, the method will test future dates accurately. If the
0480: * the threshold days value is negative, the method will test past dates.
0481: *
0482: * @param document The PaymentRequestDocument
0483: * @return True if the PREQ's pay date is not over the threshold number of days away.
0484: */
0485: public boolean validatePayDateNotOverThresholdDaysAway(
0486: PaymentRequestDocument document) {
0487: boolean valid = true;
0488: int thresholdDays = PurapConstants.PREQ_PAY_DATE_DAYS_BEFORE_WARNING;
0489: if ((document.getPaymentRequestPayDate() != null)
0490: && SpringContext.getBean(PurapService.class)
0491: .isDateMoreThanANumberOfDaysAway(
0492: document.getPaymentRequestPayDate(),
0493: thresholdDays)) {
0494: if (ObjectUtils.isNull(GlobalVariables.getMessageList())) {
0495: GlobalVariables.setMessageList(new ArrayList());
0496: }
0497: if (!GlobalVariables
0498: .getMessageList()
0499: .contains(
0500: PurapKeyConstants.WARNING_PAYMENT_REQUEST_PAYDATE_OVER_THRESHOLD_DAYS)) {
0501: GlobalVariables
0502: .getMessageList()
0503: .add(
0504: PurapKeyConstants.WARNING_PAYMENT_REQUEST_PAYDATE_OVER_THRESHOLD_DAYS);
0505: }
0506: valid &= false;
0507: }
0508: return valid;
0509: }
0510:
0511: /**
0512: * Validates whether the total of the items' extended price, excluding the item types that can be negative, match with
0513: * the vendor invoice amount that the user entered at the beginning of the preq creation, and if they don't match, the app will
0514: * just print a warning to the page that the amounts don't match.
0515: *
0516: * @param document - payment request document
0517: */
0518: public boolean validateTotals(PaymentRequestDocument document) {
0519: boolean valid = true;
0520: List excludeDiscount = new ArrayList();
0521: excludeDiscount
0522: .add(PurapConstants.ItemTypeCodes.ITEM_TYPE_PMT_TERMS_DISCOUNT_CODE);
0523: if ((ObjectUtils.isNull(document.getVendorInvoiceAmount()))
0524: || (this .getTotalExcludingItemTypes(
0525: document.getItems(), excludeDiscount)
0526: .compareTo(document.getVendorInvoiceAmount()) != 0 && !document
0527: .isUnmatchedOverride())) {
0528: if (!GlobalVariables
0529: .getMessageList()
0530: .contains(
0531: PurapKeyConstants.WARNING_PAYMENT_REQUEST_VENDOR_INVOICE_AMOUNT_INVALID)) {
0532: GlobalVariables
0533: .getMessageList()
0534: .add(
0535: PurapKeyConstants.WARNING_PAYMENT_REQUEST_VENDOR_INVOICE_AMOUNT_INVALID);
0536: valid = false;
0537: }
0538: }
0539: flagLineItemTotals(document.getItems());
0540: return valid;
0541: }
0542:
0543: /**
0544: * Calculates a total but excludes passed in item types from the totalling.
0545: *
0546: * @param itemList - list of purap items
0547: * @param excludedItemTypes - list of item types to exclude from totalling
0548: * @return
0549: */
0550: private KualiDecimal getTotalExcludingItemTypes(
0551: List<PurApItem> itemList, List<String> excludedItemTypes) {
0552: KualiDecimal total = zero;
0553: for (PurApItem item : itemList) {
0554: if (item.getExtendedPrice() != null
0555: && item.getExtendedPrice().isNonZero()) {
0556: boolean skipThisItem = false;
0557: if (excludedItemTypes.contains(item.getItemTypeCode())) {
0558: // this item type is excluded
0559: skipThisItem = true;
0560: break;
0561: }
0562: if (skipThisItem) {
0563: continue;
0564: }
0565: total = total.add(item.getExtendedPrice());
0566: }
0567: }
0568: return total;
0569: }
0570:
0571: /**
0572: * Flags with an erorr the item totals whos calculated extended price does not equal its extended price.
0573: *
0574: * @param itemList - list of purap items
0575: */
0576: private void flagLineItemTotals(List<PurApItem> itemList) {
0577: for (PurApItem purApItem : itemList) {
0578: PaymentRequestItem item = (PaymentRequestItem) purApItem;
0579: if (item.getItemQuantity() != null) {
0580: if (item.calculateExtendedPrice().compareTo(
0581: item.getExtendedPrice()) != 0) {
0582: GlobalVariables
0583: .getErrorMap()
0584: .putError(
0585: PurapConstants.ITEM_TAB_ERROR_PROPERTY,
0586: PurapKeyConstants.ERROR_PAYMENT_REQUEST_ITEM_TOTAL_NOT_EQUAL,
0587: item.getItemIdentifierString());
0588: }
0589: }
0590: }
0591: }
0592:
0593: /**
0594: * Performs item validations for the rules that are only applicable to Payment Request Document. In EPIC, we are
0595: * also doing similar steps as in this method within the validateFormatter, which is called upon Save. Therefore now we're also
0596: * calling the same validations upon Save.
0597: *
0598: * @param purapDocument - purchasing accounts payable document
0599: * @return
0600: */
0601: @Override
0602: public boolean processItemValidation(
0603: PurchasingAccountsPayableDocument purapDocument) {
0604: boolean valid = true;
0605: PaymentRequestDocument paymentRequestDocument = (PaymentRequestDocument) purapDocument;
0606: for (PurApItem item : purapDocument.getItems()) {
0607: PaymentRequestItem preqItem = (PaymentRequestItem) item;
0608: valid &= validateEachItem(paymentRequestDocument, preqItem);
0609: }
0610: return valid;
0611: }
0612:
0613: /**
0614: * Calls another validate item method and passes an identifier string from the item.
0615: *
0616: * @param paymentRequestDocument - payment request document.
0617: * @param item
0618: * @return
0619: */
0620: private boolean validateEachItem(
0621: PaymentRequestDocument paymentRequestDocument,
0622: PaymentRequestItem item) {
0623: boolean valid = true;
0624: String identifierString = item.getItemIdentifierString();
0625: valid &= validateItem(paymentRequestDocument, item,
0626: identifierString);
0627: return valid;
0628: }
0629:
0630: /**
0631: * Performs validation if full document entry not completed and peforms varying item validation.
0632: * Such as, above the line, items without accounts, items with accounts.
0633: *
0634: * @param paymentRequestDocument - payment request document
0635: * @param item - payment request item
0636: * @param identifierString - identifier string used to mark in an error map
0637: * @return
0638: */
0639: public boolean validateItem(
0640: PaymentRequestDocument paymentRequestDocument,
0641: PaymentRequestItem item, String identifierString) {
0642: boolean valid = true;
0643: // only run item validations if before full entry
0644: if (!SpringContext.getBean(PurapService.class)
0645: .isFullDocumentEntryCompleted(paymentRequestDocument)) {
0646: if (item.getItemType().isItemTypeAboveTheLineIndicator()) {
0647: valid &= validateAboveTheLineItems(item,
0648: identifierString);
0649: }
0650: valid &= validateItemWithoutAccounts(item, identifierString);
0651: }
0652: // always run account validations
0653: valid &= validateItemAccounts(paymentRequestDocument, item,
0654: identifierString);
0655: return valid;
0656: }
0657:
0658: /**
0659: * Validates above the line items.
0660: *
0661: * @param item - payment request item
0662: * @param identifierString - identifier string used to mark in an error map
0663: * @return
0664: */
0665: private boolean validateAboveTheLineItems(PaymentRequestItem item,
0666: String identifierString) {
0667: boolean valid = true;
0668: // Currently Quantity is allowed to be NULL on screen;
0669: // must be either a positive number or NULL for DB
0670: if (ObjectUtils.isNotNull(item.getItemQuantity())) {
0671: if (item.getItemQuantity().isNegative()) {
0672: // if quantity is negative give an error
0673: valid = false;
0674: GlobalVariables.getErrorMap().putError(
0675: PurapConstants.ITEM_TAB_ERROR_PROPERTY,
0676: PurapKeyConstants.ERROR_ITEM_AMOUNT_BELOW_ZERO,
0677: ItemFields.INVOICE_QUANTITY, identifierString);
0678: }
0679: if (item.getPoOutstandingQuantity().isLessThan(
0680: item.getItemQuantity())) {
0681: valid = false;
0682: GlobalVariables.getErrorMap().putError(
0683: PurapConstants.ITEM_TAB_ERROR_PROPERTY,
0684: PurapKeyConstants.ERROR_ITEM_QUANTITY_TOO_MANY,
0685: ItemFields.INVOICE_QUANTITY, identifierString,
0686: ItemFields.OPEN_QUANTITY);
0687: }
0688: }
0689: if (ObjectUtils.isNotNull(item.getExtendedPrice())
0690: && item.getExtendedPrice().isPositive()
0691: && ObjectUtils.isNotNull(item
0692: .getPoOutstandingQuantity())
0693: && item.getPoOutstandingQuantity().isPositive()) {
0694:
0695: // here we must require the user to enter some value for quantity if they want a credit amount associated
0696: if (ObjectUtils.isNull(item.getItemQuantity())
0697: || item.getItemQuantity().isZero()) {
0698: // here we have a user not entering a quantity with an extended amount but the PO has a quantity...require user to
0699: // enter a quantity
0700: valid = false;
0701: GlobalVariables.getErrorMap().putError(
0702: PurapConstants.ITEM_TAB_ERROR_PROPERTY,
0703: PurapKeyConstants.ERROR_ITEM_QUANTITY_REQUIRED,
0704: ItemFields.INVOICE_QUANTITY, identifierString,
0705: ItemFields.OPEN_QUANTITY);
0706: }
0707: }
0708:
0709: // check that non-quantity based items are not trying to pay on a zero encumbrance amount (check only prior to ap approval)
0710: if ((ObjectUtils.isNull(item.getPaymentRequest()
0711: .getPurapDocumentIdentifier()))
0712: || (PurapConstants.PaymentRequestStatuses.IN_PROCESS
0713: .equals(item.getPaymentRequest()
0714: .getStatusCode()))) {
0715: if ((item.getItemType()
0716: .isAmountBasedGeneralLedgerIndicator())
0717: && ((item.getExtendedPrice() != null) && item
0718: .getExtendedPrice().isNonZero())) {
0719: if (item.getPoOutstandingAmount() == null
0720: || item.getPoOutstandingAmount().isZero()) {
0721: valid = false;
0722: GlobalVariables
0723: .getErrorMap()
0724: .putError(
0725: PurapConstants.ITEM_TAB_ERROR_PROPERTY,
0726: PurapKeyConstants.ERROR_ITEM_AMOUNT_ALREADY_PAID,
0727: identifierString);
0728: }
0729: }
0730: }
0731:
0732: return valid;
0733: }
0734:
0735: /**
0736: * Validates that the item must contain at least one account
0737: *
0738: * @param item - payment request item
0739: * @return
0740: */
0741: public boolean validateItemWithoutAccounts(PaymentRequestItem item,
0742: String identifierString) {
0743: boolean valid = true;
0744: if (ObjectUtils.isNotNull(item.getItemUnitPrice())
0745: && (new KualiDecimal(item.getItemUnitPrice()))
0746: .isNonZero() && item.isAccountListEmpty()) {
0747: valid = false;
0748: GlobalVariables.getErrorMap().putError(
0749: PurapConstants.ITEM_TAB_ERROR_PROPERTY,
0750: PurapKeyConstants.ERROR_ITEM_ACCOUNTING_INCOMPLETE,
0751: identifierString);
0752: }
0753: return valid;
0754: }
0755:
0756: /**
0757: * Validates the totals for the item by account, that the total by each accounting line for the item, matches
0758: * the extended price on the item.
0759: *
0760: * @param paymentRequestDocument - payment request document
0761: * @param item - payment request item to validate
0762: * @param identifierString - identifier string used to mark in an error map
0763: * @return
0764: */
0765: public boolean validateItemAccounts(
0766: PaymentRequestDocument paymentRequestDocument,
0767: PaymentRequestItem item, String identifierString) {
0768: boolean valid = true;
0769: List<PurApAccountingLine> accountingLines = item
0770: .getSourceAccountingLines();
0771: KualiDecimal itemTotal = item.getExtendedPrice();
0772: KualiDecimal accountTotal = KualiDecimal.ZERO;
0773: for (PurApAccountingLine accountingLine : accountingLines) {
0774: valid &= this .processReviewAccountingLineBusinessRules(
0775: paymentRequestDocument, accountingLine);
0776: accountTotal = accountTotal.add(accountingLine.getAmount());
0777: }
0778: if (SpringContext
0779: .getBean(PurapService.class)
0780: .isFullDocumentEntryCompleted(
0781: (PaymentRequestDocument) paymentRequestDocument)) {
0782: // check amounts not percent after full entry
0783: if (accountTotal.compareTo(itemTotal) != 0) {
0784: valid = false;
0785: GlobalVariables
0786: .getErrorMap()
0787: .putError(
0788: PurapConstants.ITEM_TAB_ERROR_PROPERTY,
0789: PurapKeyConstants.ERROR_ITEM_ACCOUNTING_AMOUNT_TOTAL,
0790: identifierString);
0791: }
0792: }
0793: return valid;
0794: }
0795:
0796: /**
0797: * Validates that a cancel can occur given the current state of the document.
0798: *
0799: * @param purapDocument - purchasing accounts payable document
0800: * @return
0801: */
0802: public boolean validateCancel(
0803: PurchasingAccountsPayableDocument purapDocument) {
0804: Collection c = new ArrayList();
0805: boolean valid = true;
0806: PaymentRequestDocument pr = (PaymentRequestDocument) purapDocument;
0807: if (PurapConstants.PaymentRequestStatuses.CANCELLED_STATUSES
0808: .contains(pr.getStatusCode())) {
0809: // send ERROR: PREQ is already cancelled
0810: valid = false;
0811: GlobalVariables.getErrorMap().putError(
0812: PurapPropertyConstants.PURAP_DOC_ID,
0813: PurapKeyConstants.ERROR_CANCEL_CANCELLED);
0814: return valid;
0815: }
0816:
0817: if (ObjectUtils.isNotNull(pr.getExtractedDate())) {
0818: // send ERROR: PREQ has been extracted to Disbursement Engine
0819: valid = false;
0820: GlobalVariables.getErrorMap().putError(
0821: PurapPropertyConstants.PURAP_DOC_ID,
0822: PurapKeyConstants.ERROR_CANCEL_EXTRACTED);
0823: return valid;
0824: }
0825: if ((PurapConstants.PurchaseOrderStatuses.CLOSED.equals(pr
0826: .getPurchaseOrderDocument().getStatusCode()))
0827: && (!(PurapConstants.PaymentRequestStatuses.IN_PROCESS
0828: .equals(pr.getStatusCode())))) {
0829: // send WARNING: PREQ Can re open PO EpicConstants.PREQ_ACTION_MSSG_WARN_PROP
0830: valid = true;
0831: GlobalVariables.getMessageList().add(
0832: PurapKeyConstants.WARNING_CANCEL_REOPEN_PO);
0833: return valid;
0834: }
0835: return valid;
0836: }
0837:
0838: /**
0839: *
0840: *
0841: * @param purapDocument - purchasing accounts payable document
0842: * @return
0843: */
0844: public boolean validateRouteFiscal(
0845: PurchasingAccountsPayableDocument purapDocument) {
0846: boolean valid = true;
0847: PaymentRequestDocument paymentRequest = (PaymentRequestDocument) purapDocument;
0848: valid &= validatePaymentRequestReview(paymentRequest);
0849: return valid;
0850: }
0851:
0852: /**
0853: *
0854: *
0855: * @param paymentRequest - payment request document
0856: * @return
0857: */
0858: protected boolean validatePaymentRequestReview(
0859: PaymentRequestDocument paymentRequest) {
0860: boolean valid = true;
0861:
0862: // if FY > current FY, warn user that payment will happen in current year
0863: UniversityDateService universityDateService = SpringContext
0864: .getBean(UniversityDateService.class);
0865: Integer fiscalYear = universityDateService
0866: .getCurrentFiscalYear();
0867: Date closingDate = universityDateService
0868: .getLastDateOfFiscalYear(fiscalYear);
0869:
0870: if (paymentRequest.getPurchaseOrderDocument().getPostingYear()
0871: .intValue() > fiscalYear) {
0872: GlobalVariables.getMessageList().add(
0873: PurapKeyConstants.WARNING_ENCUMBER_NEXT_FY);
0874: }
0875:
0876: for (Iterator itemIter = paymentRequest.getItems().iterator(); itemIter
0877: .hasNext();) {
0878: PaymentRequestItem item = (PaymentRequestItem) itemIter
0879: .next();
0880:
0881: boolean containsAccounts = false;
0882: int accountLineNbr = 0;
0883:
0884: String identifier = item.getItemIdentifierString();
0885: BigDecimal total = BigDecimal.ZERO;
0886: LOG.debug("validatePaymentRequestReview() The "
0887: + identifier
0888: + " is getting the total percent field set to "
0889: + BigDecimal.ZERO);
0890:
0891: if (((item.getExtendedPrice() != null && item
0892: .getExtendedPrice().isNonZero())
0893: && item.getItemType()
0894: .isItemTypeAboveTheLineIndicator() && ((item
0895: .getItemType()
0896: .isAmountBasedGeneralLedgerIndicator() && (item
0897: .getPoOutstandingAmount() != null && item
0898: .getPoOutstandingAmount().isNonZero())) || (item
0899: .getItemType()
0900: .isQuantityBasedGeneralLedgerIndicator() && (item
0901: .getPoOutstandingQuantity() != null && item
0902: .getPoOutstandingQuantity().isNonZero()))))
0903: || (((item.getExtendedPrice() != null) && (item
0904: .getExtendedPrice().isNonZero())) && (!item
0905: .getItemType()
0906: .isItemTypeAboveTheLineIndicator()))) {
0907: // OK TO VALIDATE because we have extended price and an open encumberance on the PO item
0908: super .processItemValidation(paymentRequest);
0909: // This is calling the validations which in EPIC are located in validateFormatters, but in Kuali they should be
0910: // covered
0911: // within the processItemValidation of this class.
0912: validateEachItem(paymentRequest, item);
0913: } else if ((item.getExtendedPrice() != null
0914: && item.getExtendedPrice().isNonZero()
0915: && item.getItemType()
0916: .isItemTypeAboveTheLineIndicator() && ((item
0917: .getItemType()
0918: .isAmountBasedGeneralLedgerIndicator() && (item
0919: .getPoOutstandingAmount() == null || item
0920: .getPoOutstandingAmount().isZero())) || (item
0921: .getItemType()
0922: .isQuantityBasedGeneralLedgerIndicator() && (item
0923: .getPoOutstandingQuantity() == null || item
0924: .getPoOutstandingQuantity().isZero()))))) {
0925: // ERROR because we have extended price and no open encumberance on the PO item
0926: // this error should have been caught at an earlier level
0927: if (item.getItemType()
0928: .isAmountBasedGeneralLedgerIndicator()) {
0929: String error = "Payment Request "
0930: + paymentRequest
0931: .getPurapDocumentIdentifier()
0932: + ", " + identifier
0933: + " has extended price '"
0934: + item.getExtendedPrice()
0935: + "' but outstanding encumbered amount "
0936: + item.getPoOutstandingAmount();
0937: LOG
0938: .error("validatePaymentRequestReview() "
0939: + error);
0940: } else {
0941: String error = "Payment Request "
0942: + paymentRequest
0943: .getPurapDocumentIdentifier()
0944: + ", " + identifier + " has quantity '"
0945: + item.getItemQuantity()
0946: + "' but outstanding encumbered quantity "
0947: + item.getPoOutstandingQuantity();
0948: LOG
0949: .error("validatePaymentRequestReview() "
0950: + error);
0951: }
0952: } else {
0953: // not validating but ok
0954: String error = "Payment Request "
0955: + paymentRequest.getPurapDocumentIdentifier()
0956: + ", " + identifier + " has extended price '"
0957: + item.getExtendedPrice() + "'";
0958: if (item.getItemType()
0959: .isItemTypeAboveTheLineIndicator()) {
0960: if (item.getItemType()
0961: .isAmountBasedGeneralLedgerIndicator()) {
0962: error = error
0963: + " with outstanding encumbered amount "
0964: + item.getPoOutstandingAmount();
0965: } else {
0966: error = error
0967: + " with outstanding encumbered quantity "
0968: + item.getPoOutstandingQuantity();
0969: }
0970: }
0971: LOG.info("validatePaymentRequestReview() " + error);
0972: }
0973:
0974: }
0975: return valid;
0976: }
0977:
0978: /**
0979: * Forces general ledger entries to be approved, does not wait for payment request document final approval.
0980: *
0981: * @see org.kuali.module.purap.rules.PurapAccountingDocumentRuleBase#customizeExplicitGeneralLedgerPendingEntry(org.kuali.kfs.document.AccountingDocument, org.kuali.kfs.bo.AccountingLine, org.kuali.kfs.bo.GeneralLedgerPendingEntry)
0982: */
0983: @Override
0984: protected void customizeExplicitGeneralLedgerPendingEntry(
0985: AccountingDocument accountingDocument,
0986: AccountingLine accountingLine,
0987: GeneralLedgerPendingEntry explicitEntry) {
0988: super .customizeExplicitGeneralLedgerPendingEntry(
0989: accountingDocument, accountingLine, explicitEntry);
0990:
0991: PaymentRequestDocument preq = (PaymentRequestDocument) accountingDocument;
0992:
0993: SpringContext.getBean(PurapGeneralLedgerService.class)
0994: .customizeGeneralLedgerPendingEntry(preq,
0995: accountingLine, explicitEntry,
0996: preq.getPurchaseOrderIdentifier(),
0997: preq.getDebitCreditCodeForGLEntries(),
0998: PurapDocTypeCodes.PAYMENT_REQUEST_DOCUMENT,
0999: preq.isGenerateEncumbranceEntries());
1000:
1001: // PREQs do not wait for document final approval to post GL entries; here we are forcing them to be APPROVED
1002: explicitEntry
1003: .setFinancialDocumentApprovedCode(KFSConstants.PENDING_ENTRY_APPROVED_STATUS_CODE.APPROVED);
1004: }
1005:
1006: /**
1007: * Returns true if full document entry is completed and bypasses any further validation, otherwise proceeds as normal.
1008: *
1009: * @see org.kuali.module.purap.rules.PurchasingAccountsPayableDocumentRuleBase#verifyAccountPercent(org.kuali.kfs.document.AccountingDocument, java.util.List, java.lang.String)
1010: */
1011: @Override
1012: protected boolean verifyAccountPercent(
1013: AccountingDocument accountingDocument,
1014: List<PurApAccountingLine> purAccounts, String itemLineNumber) {
1015: if (SpringContext.getBean(PurapService.class)
1016: .isFullDocumentEntryCompleted(
1017: (PaymentRequestDocument) accountingDocument)) {
1018: return true;
1019: }
1020: return super .verifyAccountPercent(accountingDocument,
1021: purAccounts, itemLineNumber);
1022: }
1023:
1024: /**
1025: * @see org.kuali.module.purap.rule.CancelAccountsPayableRule#processCancelAccountsPayableBusinessRules(org.kuali.module.purap.document.AccountsPayableDocument)
1026: */
1027: public boolean processCancelAccountsPayableBusinessRules(
1028: AccountsPayableDocument document) {
1029: boolean valid = true;
1030: PaymentRequestDocument preq = (PaymentRequestDocument) document;
1031: // no errors for now since we are not showing the button if they can't cancel, if that changes we need errors
1032: // also this is different than CreditMemo even though the rules are almost identical we should merge and have one consistent
1033: // way to do this
1034: PaymentRequestDocumentActionAuthorizer preqAuth = new PaymentRequestDocumentActionAuthorizer(
1035: preq);
1036: valid = valid &= preqAuth.canCancel();
1037: return valid;
1038: }
1039: }
|