org.kuali.kfs.module.purap.document.service.impl.PaymentRequestServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.kuali.kfs.module.purap.document.service.impl.PaymentRequestServiceImpl.java

Source

/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 * 
 * Copyright 2005-2014 The Kuali Foundation
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.module.purap.document.service.impl;

import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.module.purap.PurapConstants;
import org.kuali.kfs.module.purap.PurapConstants.ItemTypeCodes;
import org.kuali.kfs.module.purap.PurapConstants.PREQDocumentsStrings;
import org.kuali.kfs.module.purap.PurapConstants.PaymentRequestStatuses;
import org.kuali.kfs.module.purap.PurapKeyConstants;
import org.kuali.kfs.module.purap.PurapParameterConstants;
import org.kuali.kfs.module.purap.PurapParameterConstants.NRATaxParameters;
import org.kuali.kfs.module.purap.PurapPropertyConstants;
import org.kuali.kfs.module.purap.PurapRuleConstants;
import org.kuali.kfs.module.purap.businessobject.AutoApproveExclude;
import org.kuali.kfs.module.purap.businessobject.ItemType;
import org.kuali.kfs.module.purap.businessobject.NegativePaymentRequestApprovalLimit;
import org.kuali.kfs.module.purap.businessobject.PaymentRequestAccount;
import org.kuali.kfs.module.purap.businessobject.PaymentRequestItem;
import org.kuali.kfs.module.purap.businessobject.PurApAccountingLine;
import org.kuali.kfs.module.purap.businessobject.PurApItem;
import org.kuali.kfs.module.purap.businessobject.PurchaseOrderItem;
import org.kuali.kfs.module.purap.document.AccountsPayableDocument;
import org.kuali.kfs.module.purap.document.PaymentRequestDocument;
import org.kuali.kfs.module.purap.document.PurchaseOrderDocument;
import org.kuali.kfs.module.purap.document.VendorCreditMemoDocument;
import org.kuali.kfs.module.purap.document.dataaccess.PaymentRequestDao;
import org.kuali.kfs.module.purap.document.service.AccountsPayableService;
import org.kuali.kfs.module.purap.document.service.NegativePaymentRequestApprovalLimitService;
import org.kuali.kfs.module.purap.document.service.PaymentRequestService;
import org.kuali.kfs.module.purap.document.service.PurApWorkflowIntegrationService;
import org.kuali.kfs.module.purap.document.service.PurapService;
import org.kuali.kfs.module.purap.document.service.PurchaseOrderService;
import org.kuali.kfs.module.purap.document.validation.event.AttributedContinuePurapEvent;
import org.kuali.kfs.module.purap.document.validation.event.PurchasingAccountsPayableItemPreCalculateEvent;
import org.kuali.kfs.module.purap.exception.PurError;
import org.kuali.kfs.module.purap.service.PurapAccountingService;
import org.kuali.kfs.module.purap.service.PurapGeneralLedgerService;
import org.kuali.kfs.module.purap.util.ExpiredOrClosedAccountEntry;
import org.kuali.kfs.module.purap.util.PurApItemUtils;
import org.kuali.kfs.module.purap.util.VendorGroupingHelper;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.AccountingLine;
import org.kuali.kfs.sys.businessobject.Bank;
import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.service.BankService;
import org.kuali.kfs.sys.service.FinancialSystemWorkflowHelperService;
import org.kuali.kfs.sys.service.NonTransactional;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
import org.kuali.kfs.vnd.VendorConstants;
import org.kuali.kfs.vnd.businessobject.PaymentTermType;
import org.kuali.kfs.vnd.businessobject.VendorAddress;
import org.kuali.kfs.vnd.businessobject.VendorDetail;
import org.kuali.kfs.vnd.document.service.VendorService;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.coreservice.framework.parameter.ParameterService;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kim.api.identity.Person;
import org.kuali.rice.kim.api.identity.PersonService;
import org.kuali.rice.kns.service.DataDictionaryService;
import org.kuali.rice.krad.bo.DocumentHeader;
import org.kuali.rice.krad.bo.Note;
import org.kuali.rice.krad.exception.InfrastructureException;
import org.kuali.rice.krad.exception.ValidationException;
import org.kuali.rice.krad.service.BusinessObjectService;
import org.kuali.rice.krad.service.DocumentService;
import org.kuali.rice.krad.service.KualiRuleService;
import org.kuali.rice.krad.service.NoteService;
import org.kuali.rice.krad.util.GlobalVariables;
import org.kuali.rice.krad.util.KRADPropertyConstants;
import org.kuali.rice.krad.util.ObjectUtils;
import org.kuali.rice.krad.workflow.service.WorkflowDocumentService;
import org.springframework.transaction.annotation.Transactional;

/**
 * This class provides services of use to a payment request document
 */

public class PaymentRequestServiceImpl implements PaymentRequestService {
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
            .getLogger(PaymentRequestServiceImpl.class);

    protected DateTimeService dateTimeService;
    protected DocumentService documentService;
    protected NoteService noteService;
    protected PurapService purapService;
    protected PaymentRequestDao paymentRequestDao;
    protected ParameterService parameterService;
    protected ConfigurationService configurationService;
    protected NegativePaymentRequestApprovalLimitService negativePaymentRequestApprovalLimitService;
    protected PurapAccountingService purapAccountingService;
    protected BusinessObjectService businessObjectService;
    protected PurApWorkflowIntegrationService purapWorkflowIntegrationService;
    protected WorkflowDocumentService workflowDocumentService;
    protected AccountsPayableService accountsPayableService;
    protected VendorService vendorService;
    protected DataDictionaryService dataDictionaryService;
    protected UniversityDateService universityDateService;
    protected BankService bankService;
    protected PurchaseOrderService purchaseOrderService;
    protected FinancialSystemWorkflowHelperService financialSystemWorkflowHelperService;
    protected KualiRuleService kualiRuleService;
    protected PersonService personService;

    /**
     * NOTE: unused
     *
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#getPaymentRequestsToExtractByCM(java.lang.String, org.kuali.kfs.module.purap.document.VendorCreditMemoDocument)
     */
    @Override
    @Deprecated
    @NonTransactional
    public Collection<PaymentRequestDocument> getPaymentRequestsToExtractByCM(String campusCode,
            VendorCreditMemoDocument cmd) {
        LOG.debug("getPaymentRequestsByCM() started");
        Date currentSqlDateMidnight = dateTimeService.getCurrentSqlDateMidnight();
        List<PaymentRequestDocument> paymentRequestIterator = paymentRequestDao.getPaymentRequestsToExtract(
                campusCode, null, null, cmd.getVendorHeaderGeneratedIdentifier(),
                cmd.getVendorDetailAssignedIdentifier(), currentSqlDateMidnight);

        return filterPaymentRequestByAppDocStatus(paymentRequestIterator,
                PurapConstants.PaymentRequestStatuses.APPDOC_AUTO_APPROVED,
                PurapConstants.PaymentRequestStatuses.APPDOC_DEPARTMENT_APPROVED);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#getPaymentRequestsToExtractByVendor(java.lang.String,
     *      org.kuali.kfs.module.purap.util.VendorGroupingHelper, java.sql.Date)
     */
    @Override
    @NonTransactional
    public Collection<PaymentRequestDocument> getPaymentRequestsToExtractByVendor(String campusCode,
            VendorGroupingHelper vendor, Date onOrBeforePaymentRequestPayDate) {
        LOG.debug("getPaymentRequestsByVendor() started");
        Collection<PaymentRequestDocument> paymentRequestDocuments = paymentRequestDao
                .getPaymentRequestsToExtractForVendor(campusCode, vendor, onOrBeforePaymentRequestPayDate);

        return filterPaymentRequestByAppDocStatus(paymentRequestDocuments,
                PurapConstants.PaymentRequestStatuses.APPDOC_AUTO_APPROVED,
                PurapConstants.PaymentRequestStatuses.APPDOC_DEPARTMENT_APPROVED);
    }

    /**
     * @see org.kuali.module.purap.server.PaymentRequestService.getPaymentRequestsToExtract(Date)
     */
    @Override
    @NonTransactional
    public Collection<PaymentRequestDocument> getPaymentRequestsToExtract(Date onOrBeforePaymentRequestPayDate) {
        LOG.debug("getPaymentRequestsToExtract() started");

        Collection<PaymentRequestDocument> paymentRequestIterator = paymentRequestDao
                .getPaymentRequestsToExtract(false, null, onOrBeforePaymentRequestPayDate);
        return filterPaymentRequestByAppDocStatus(paymentRequestIterator,
                PaymentRequestStatuses.STATUSES_ALLOWED_FOR_EXTRACTION);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#getPaymentRequestsToExtractSpecialPayments(java.lang.String,
     *      java.sql.Date)
     */
    @Override
    @NonTransactional
    public Collection<PaymentRequestDocument> getPaymentRequestsToExtractSpecialPayments(String chartCode,
            Date onOrBeforePaymentRequestPayDate) {
        LOG.debug("getPaymentRequestsToExtractSpecialPayments() started");

        Collection<PaymentRequestDocument> paymentRequestIterator = paymentRequestDao
                .getPaymentRequestsToExtract(true, chartCode, onOrBeforePaymentRequestPayDate);
        return filterPaymentRequestByAppDocStatus(paymentRequestIterator,
                PaymentRequestStatuses.STATUSES_ALLOWED_FOR_EXTRACTION);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#getImmediatePaymentRequestsToExtract(java.lang.String)
     */
    @Override
    @NonTransactional
    public Collection<PaymentRequestDocument> getImmediatePaymentRequestsToExtract(String chartCode) {
        LOG.debug("getImmediatePaymentRequestsToExtract() started");

        Collection<PaymentRequestDocument> paymentRequestIterator = paymentRequestDao
                .getImmediatePaymentRequestsToExtract(chartCode);
        return filterPaymentRequestByAppDocStatus(paymentRequestIterator,
                PaymentRequestStatuses.STATUSES_ALLOWED_FOR_EXTRACTION);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#getPaymentRequestToExtractByChart(java.lang.String,
     *      java.sql.Date)
     */
    @Override
    @NonTransactional
    public Collection<PaymentRequestDocument> getPaymentRequestToExtractByChart(String chartCode,
            Date onOrBeforePaymentRequestPayDate) {
        LOG.debug("getPaymentRequestToExtractByChart() started");

        Collection<PaymentRequestDocument> paymentRequestIterator = paymentRequestDao
                .getPaymentRequestsToExtract(false, chartCode, onOrBeforePaymentRequestPayDate);
        return filterPaymentRequestByAppDocStatus(paymentRequestIterator,
                PaymentRequestStatuses.STATUSES_ALLOWED_FOR_EXTRACTION);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService.autoApprovePaymentRequests()
     */
    @Override
    @NonTransactional
    public boolean autoApprovePaymentRequests() {
        if (LOG.isInfoEnabled()) {
            LOG.info("Starting autoApprovePaymentRequests.");
        }
        boolean hadErrorAtLeastOneError = true;
        // should objects from existing user session be copied over

        Date todayAtMidnight = dateTimeService.getCurrentSqlDateMidnight();
        List<String> docNumbers = paymentRequestDao.getEligibleForAutoApproval(todayAtMidnight);
        if (LOG.isInfoEnabled()) {
            LOG.info(" -- Initial filtering complete, returned " + new Integer(docNumbers.size()).toString()
                    + " docs.");
        }

        String samt = parameterService.getParameterValueAsString(PaymentRequestDocument.class,
                PurapParameterConstants.PURAP_DEFAULT_NEGATIVE_PAYMENT_REQUEST_APPROVAL_LIMIT);
        KualiDecimal defaultMinimumLimit = new KualiDecimal(samt);
        if (LOG.isInfoEnabled()) {
            LOG.info(" -- Using default limit value of " + defaultMinimumLimit.toString() + ".");
        }

        List<PaymentRequestDocument> docs = new ArrayList<PaymentRequestDocument>();
        for (String docNumber : docNumbers) {
            PaymentRequestDocument paymentRequestDocument = getPaymentRequestByDocumentNumber(docNumber);
            if (ObjectUtils.isNotNull(paymentRequestDocument)) {
                hadErrorAtLeastOneError |= !autoApprovePaymentRequest(paymentRequestDocument, defaultMinimumLimit);
            }
        }
        return hadErrorAtLeastOneError;
    }

    /**
     * NOTE: in the event of auto-approval failure, this method may throw a RuntimeException, indicating to Spring transactional
     * management that the transaction should be rolled back.
     *
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#autoApprovePaymentRequest(java.lang.String,
     *      org.kuali.rice.core.api.util.type.KualiDecimal)
     */
    @Override
    @NonTransactional
    public boolean autoApprovePaymentRequest(String docNumber, KualiDecimal defaultMinimumLimit) {
        PaymentRequestDocument paymentRequestDocument = null;
        try {
            paymentRequestDocument = (PaymentRequestDocument) documentService.getByDocumentHeaderId(docNumber);
            if (paymentRequestDocument.isHoldIndicator()
                    || paymentRequestDocument.isPaymentRequestedCancelIndicator()
                    || !Arrays.asList(PurapConstants.PaymentRequestStatuses.PREQ_STATUSES_FOR_AUTO_APPROVE)
                            .contains(paymentRequestDocument.getApplicationDocumentStatus())) {
                // this condition is based on the conditions that PaymentRequestDaoOjb.getEligibleDocumentNumbersForAutoApproval()
                // uses to query
                // the database. Rechecking these conditions to ensure that the document is eligible for auto-approval, because
                // we're not running things
                // within the same transaction anymore and changes could have occurred since we called that method that make this
                // document not auto-approvable

                // note that this block does not catch all race conditions
                // however, this error condition is not enough to make us return an error code, so just skip the document
                LOG.warn("Payment Request Document " + paymentRequestDocument.getDocumentNumber()
                        + " could not be auto-approved because it has either been placed on hold, "
                        + " requested cancel, or does not have one of the PREQ statuses for auto-approve.");
                return true;
            }
            if (autoApprovePaymentRequest(paymentRequestDocument, defaultMinimumLimit)) {
                if (LOG.isInfoEnabled()) {
                    LOG.info("Auto-approval for payment request successful.  Doc number: " + docNumber);
                }
                return true;
            } else {
                LOG.error("Payment Request Document " + docNumber + " could not be auto-approved.");
                return false;
            }
        } catch (WorkflowException we) {
            LOG.error("Exception encountered when retrieving document number " + docNumber + ".", we);
            // throw a runtime exception up so that we can force a rollback
            throw new RuntimeException("Exception encountered when retrieving document number " + docNumber + ".",
                    we);
        }
    }

    /**
     * NOTE: in the event of auto-approval failure, this method may throw a RuntimeException, indicating to Spring transactional
     * management that the transaction should be rolled back.
     *
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#autoApprovePaymentRequest(org.kuali.kfs.module.purap.document.PaymentRequestDocument,
     *      org.kuali.rice.core.api.util.type.KualiDecimal)
     */
    @Override
    @Transactional
    public boolean autoApprovePaymentRequest(PaymentRequestDocument doc, KualiDecimal defaultMinimumLimit) {
        if (isEligibleForAutoApproval(doc, defaultMinimumLimit)) {
            try {
                // Much of the rice frameworks assumes that document instances that are saved via DocumentService.saveDocument are
                // those
                // that were dynamically created by PojoFormBase (i.e., the Document instance wasn't created from OJB). We need to
                // make
                // a deep copy and materialize collections to fulfill that assumption so that collection elements will delete
                // properly

                // TODO: maybe rewriting PurapService.calculateItemTax could be rewritten so that the a deep copy doesn't need to be
                // made
                // by taking advantage of OJB's managed array lists
                try {
                    ObjectUtils.materializeUpdateableCollections(doc);
                    for (PaymentRequestItem item : (List<PaymentRequestItem>) doc.getItems()) {
                        ObjectUtils.materializeUpdateableCollections(item);
                    }
                } catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
                doc = (PaymentRequestDocument) ObjectUtils.deepCopy(doc);

                //set the auto approved indicator to true so that doRouteStatus method can use to
                //change the app doc status.
                doc.setAutoApprovedIndicator(true);
                LOG.info("About to blanketApproveDocument, doc.getDocumentNumber()=" + doc.getDocumentNumber());
                // su approve rather than blanket approve, so no ACK notifications would be generated by Rice
                documentService.superUserApproveDocument(doc, "auto-approving: Total is below threshold.");
            } catch (WorkflowException we) {
                LOG.error("Exception encountered when approving document number " + doc.getDocumentNumber() + ".",
                        we);
                // throw a runtime exception up so that we can force a rollback
                throw new RuntimeException(
                        "Exception encountered when approving document number " + doc.getDocumentNumber() + ".",
                        we);
            }
        }
        return true;
    }

    /**
     * Determines whether or not a payment request document can be automatically approved. FYI - If fiscal reviewers are allowed to
     * save account changes without the full account validation running then this method must call full account validation to make
     * sure auto approver is not blanket approving an invalid document according the the accounts on the items
     *
     * @param document The payment request document to be determined whether it can be automatically approved.
     * @param defaultMinimumLimit The amount to be used as the minimum amount if no limit was found or the default is less than the
     *        limit.
     * @return boolean true if the payment request document is eligible for auto approval.
     */
    protected boolean isEligibleForAutoApproval(PaymentRequestDocument document, KualiDecimal defaultMinimumLimit) {
        // Check if vendor is foreign.
        if (document.getVendorDetail().getVendorHeader().getVendorForeignIndicator().booleanValue()) {
            if (LOG.isInfoEnabled()) {
                LOG.info(" -- PayReq [" + document.getDocumentNumber() + "] skipped due to a Foreign Vendor.");
            }
            return false;
        }

        // check to make sure the payment request isn't scheduled to stop in tax review.
        if (purapWorkflowIntegrationService.willDocumentStopAtGivenFutureRouteNode(document,
                PaymentRequestStatuses.NODE_VENDOR_TAX_REVIEW)) {
            if (LOG.isInfoEnabled()) {
                LOG.info(" -- PayReq [" + document.getDocumentNumber() + "] skipped due to requiring Tax Review.");
            }
            return false;
        }

        // Change to not auto approve if positive approval required indicator set to Yes
        if (document.isPaymentRequestPositiveApprovalIndicator()) {
            if (LOG.isInfoEnabled()) {
                LOG.info(" -- PayReq [" + document.getDocumentNumber()
                        + "] skipped due to a Positive Approval Required Indicator set to Yes.");
            }
            return false;
        }

        // This minimum will be set to the minimum limit derived from all
        // accounting lines on the document. If no limit is determined, the
        // default will be used.
        KualiDecimal minimumAmount = null;

        // Iterate all source accounting lines on the document, deriving a
        // minimum limit from each according to chart, chart and account, and
        // chart and organization.
        final List<SourceAccountingLine> summaryLines = purapAccountingService.generateSummary(document.getItems());
        for (SourceAccountingLine line : summaryLines) {
            // check to make sure the account is in the auto approve exclusion list
            Map<String, Object> autoApproveMap = new HashMap<String, Object>();
            autoApproveMap.put("chartOfAccountsCode", line.getChartOfAccountsCode());
            autoApproveMap.put("accountNumber", line.getAccountNumber());
            autoApproveMap.put("active", true);
            AutoApproveExclude autoApproveExclude = businessObjectService.findByPrimaryKey(AutoApproveExclude.class,
                    autoApproveMap);
            if (autoApproveExclude != null) {
                if (LOG.isInfoEnabled()) {
                    LOG.info(" -- PayReq [" + document.getDocumentNumber()
                            + "] skipped due to source accounting line " + line.getSequenceNumber()
                            + " using Chart/Account [" + line.getChartOfAccountsCode() + "-"
                            + line.getAccountNumber()
                            + "], which is excluded in the Auto Approve Exclusions table.");
                }
                return false;
            }

            minimumAmount = getMinimumLimitAmount(
                    negativePaymentRequestApprovalLimitService.findByChart(line.getChartOfAccountsCode()),
                    minimumAmount);
            minimumAmount = getMinimumLimitAmount(negativePaymentRequestApprovalLimitService
                    .findByChartAndAccount(line.getChartOfAccountsCode(), line.getAccountNumber()), minimumAmount);
            minimumAmount = getMinimumLimitAmount(negativePaymentRequestApprovalLimitService
                    .findByChartAndOrganization(line.getChartOfAccountsCode(), line.getOrganizationReferenceId()),
                    minimumAmount);
        }

        // If Receiving required is set, it's not needed to check the negative payment request approval limit
        if (document.isReceivingDocumentRequiredIndicator()) {
            if (LOG.isInfoEnabled()) {
                LOG.info(" -- PayReq [" + document.getDocumentNumber()
                        + "] auto-approved (ignored dollar limit) due to Receiving Document Required Indicator set to Yes.");
            }
            return true;
        }

        // If no limit was found or the default is less than the limit, the default limit is used.
        if (ObjectUtils.isNull(minimumAmount) || defaultMinimumLimit.compareTo(minimumAmount) < 0) {
            minimumAmount = defaultMinimumLimit;
        }

        // The document is eligible for auto-approval if the document total is below the limit.
        if (document.getFinancialSystemDocumentHeader().getFinancialDocumentTotalAmount()
                .isLessThan(minimumAmount)) {
            if (LOG.isInfoEnabled()) {
                LOG.info(" -- PayReq [" + document.getDocumentNumber() + "] auto-approved due to document Total ["
                        + document.getFinancialSystemDocumentHeader().getFinancialDocumentTotalAmount()
                        + "] being less than "
                        + (minimumAmount == defaultMinimumLimit ? "Default Auto-Approval Limit "
                                : "Configured Auto-Approval Limit ")
                        + "of " + (minimumAmount == null ? "null" : minimumAmount.toString()) + ".");
            }
            return true;
        }

        if (LOG.isInfoEnabled()) {
            LOG.info(" -- PayReq [" + document.getDocumentNumber() + "] skipped due to document Total ["
                    + document.getFinancialSystemDocumentHeader().getFinancialDocumentTotalAmount()
                    + "] being greater than "
                    + (minimumAmount == defaultMinimumLimit ? "Default Auto-Approval Limit "
                            : "Configured Auto-Approval Limit ")
                    + "of " + (minimumAmount == null ? "null" : minimumAmount.toString()) + ".");
        }

        return false;
    }

    /**
     * This method iterates a collection of negative payment request approval limits and returns the minimum of a given minimum
     * amount and the least among the limits in the collection.
     *
     * @param limits The collection of NegativePaymentRequestApprovalLimit to be used in determining the minimum limit amount.
     * @param minimumAmount The amount to be compared with the collection of NegativePaymentRequestApprovalLimit to determine the
     *        minimum limit amount.
     * @return The minimum of the given minimum amount and the least among the limits in the collection.
     */
    protected KualiDecimal getMinimumLimitAmount(Collection<NegativePaymentRequestApprovalLimit> limits,
            KualiDecimal minimumAmount) {
        for (NegativePaymentRequestApprovalLimit limit : limits) {
            KualiDecimal amount = limit.getNegativePaymentRequestApprovalLimitAmount();
            if (null == minimumAmount) {
                minimumAmount = amount;
            } else if (minimumAmount.isGreaterThan(amount)) {
                minimumAmount = amount;
            }
        }
        return minimumAmount;
    }

    /**
     * Retrieves a list of payment request documents with the given vendor id and invoice number.
     *
     * @param vendorHeaderGeneratedId The vendor header generated id.
     * @param vendorDetailAssignedId The vendor detail assigned id.
     * @param invoiceNumber The invoice number as entered by AP.
     * @return List of payment request document.
     */
    @Override
    @NonTransactional
    public List getPaymentRequestsByVendorNumber(Integer vendorHeaderGeneratedId, Integer vendorDetailAssignedId) {
        LOG.debug("getActivePaymentRequestsByVendorNumber() started");
        return paymentRequestDao.getActivePaymentRequestsByVendorNumber(vendorHeaderGeneratedId,
                vendorDetailAssignedId);
    }

    /**
     * Retrieves a list of payment request documents with the given vendor id and invoice number.
     *
     * @param vendorHeaderGeneratedId The vendor header generated id.
     * @param vendorDetailAssignedId The vendor detail assigned id.
     * @param invoiceNumber The invoice number as entered by AP.
     * @return List of payment request document.
     */
    @Override
    @NonTransactional
    public List getPaymentRequestsByVendorNumberInvoiceNumber(Integer vendorHeaderGeneratedId,
            Integer vendorDetailAssignedId, String invoiceNumber) {
        LOG.debug("getActivePaymentRequestsByVendorNumberInvoiceNumber() started");
        return paymentRequestDao.getActivePaymentRequestsByVendorNumberInvoiceNumber(vendorHeaderGeneratedId,
                vendorDetailAssignedId, invoiceNumber);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#paymentRequestDuplicateMessages(org.kuali.kfs.module.purap.document.PaymentRequestDocument)
     */
    @Override
    @NonTransactional
    public HashMap<String, String> paymentRequestDuplicateMessages(PaymentRequestDocument document) {
        HashMap<String, String> msgs;
        msgs = new HashMap<String, String>();

        Integer purchaseOrderId = document.getPurchaseOrderIdentifier();

        if (ObjectUtils.isNotNull(document.getInvoiceDate())) {
            if (purapService.isDateAYearBeforeToday(document.getInvoiceDate())) {
                msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService
                        .getPropertyValueAsString(PurapKeyConstants.MESSAGE_INVOICE_DATE_A_YEAR_OR_MORE_PAST));
            }
        }
        PurchaseOrderDocument po = document.getPurchaseOrderDocument();

        if (po != null) {
            Integer vendorDetailAssignedId = po.getVendorDetailAssignedIdentifier();
            Integer vendorHeaderGeneratedId = po.getVendorHeaderGeneratedIdentifier();

            List<PaymentRequestDocument> preqs = new ArrayList();

            List<PaymentRequestDocument> preqsDuplicates = getPaymentRequestsByVendorNumber(vendorHeaderGeneratedId,
                    vendorDetailAssignedId);
            for (PaymentRequestDocument duplicatePREQ : preqsDuplicates) {
                if (duplicatePREQ.getInvoiceNumber().toUpperCase()
                        .equals(document.getInvoiceNumber().toUpperCase())) {
                    // found the duplicate row... so add to the preqs list...
                    preqs.add(duplicatePREQ);
                }
            }

            if (preqs.size() > 0) {
                boolean addedMessage = false;
                boolean foundCanceledPostApprove = false; // cancelled
                boolean foundCanceledPreApprove = false; // voided
                for (PaymentRequestDocument testPREQ : preqs) {
                    if (StringUtils.equals(testPREQ.getApplicationDocumentStatus(),
                            PaymentRequestStatuses.APPDOC_CANCELLED_POST_AP_APPROVE)) {
                        foundCanceledPostApprove |= true;
                    } else if (StringUtils.equals(testPREQ.getApplicationDocumentStatus(),
                            PaymentRequestStatuses.APPDOC_CANCELLED_IN_PROCESS)) {
                        foundCanceledPreApprove |= true;
                    } else {
                        msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService
                                .getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE));
                        addedMessage = true;
                        break;
                    }
                }
                // Custom error message for duplicates related to cancelled/voided PREQs
                if (!addedMessage) {
                    if (foundCanceledPostApprove && foundCanceledPreApprove) {
                        msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION,
                                configurationService.getPropertyValueAsString(
                                        PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_CANCELLEDORVOIDED));
                    } else if (foundCanceledPreApprove) {
                        msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService
                                .getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_VOIDED));
                    } else if (foundCanceledPostApprove) {
                        // messages.add("errors.duplicate.vendor.invoice.cancelled");
                        msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService
                                .getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_CANCELLED));
                    }
                }
            }

            // Check that the invoice date and invoice total amount entered are not on any existing non-cancelled PREQs for this PO
            preqs = getPaymentRequestsByPOIdInvoiceAmountInvoiceDate(purchaseOrderId,
                    document.getVendorInvoiceAmount(), document.getInvoiceDate());
            if (preqs.size() > 0) {
                boolean addedMessage = false;
                boolean foundCanceledPostApprove = false; // cancelled
                boolean foundCanceledPreApprove = false; // voided
                msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService
                        .getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_DATE_AMOUNT));
                for (PaymentRequestDocument testPREQ : preqs) {
                    if (StringUtils.equalsIgnoreCase(testPREQ.getApplicationDocumentStatus(),
                            PaymentRequestStatuses.APPDOC_CANCELLED_POST_AP_APPROVE)) {
                        foundCanceledPostApprove |= true;
                    } else if (StringUtils.equalsIgnoreCase(testPREQ.getApplicationDocumentStatus(),
                            PaymentRequestStatuses.APPDOC_CANCELLED_IN_PROCESS)) {
                        foundCanceledPreApprove |= true;
                    } else {
                        msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService
                                .getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_DATE_AMOUNT));
                        addedMessage = true;
                        break;
                    }
                }

                // Custom error message for duplicates related to cancelled/voided PREQs
                if (!addedMessage) {
                    if (foundCanceledPostApprove && foundCanceledPreApprove) {
                        msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION,
                                configurationService.getPropertyValueAsString(
                                        PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_DATE_AMOUNT_CANCELLEDORVOIDED));
                    } else if (foundCanceledPreApprove) {
                        msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION,
                                configurationService.getPropertyValueAsString(
                                        PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_DATE_AMOUNT_VOIDED));
                        addedMessage = true;
                    } else if (foundCanceledPostApprove) {
                        msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION,
                                configurationService.getPropertyValueAsString(
                                        PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_DATE_AMOUNT_CANCELLED));
                        addedMessage = true;
                    }

                }
            }
        }
        return msgs;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#getPaymentRequestByDocumentNumber(java.lang.String)
     */
    @Override
    @NonTransactional
    public PaymentRequestDocument getPaymentRequestByDocumentNumber(String documentNumber) {
        LOG.debug("getPaymentRequestByDocumentNumber() started");

        if (ObjectUtils.isNotNull(documentNumber)) {
            try {
                PaymentRequestDocument doc = (PaymentRequestDocument) documentService
                        .getByDocumentHeaderId(documentNumber);
                return doc;
            } catch (WorkflowException e) {
                String errorMessage = "Error getting payment request document from document service";
                LOG.error("getPaymentRequestByDocumentNumber() " + errorMessage, e);
                throw new RuntimeException(errorMessage, e);
            }
        }
        return null;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#getPaymentRequestById(java.lang.Integer)
     */
    @Override
    @NonTransactional
    public PaymentRequestDocument getPaymentRequestById(Integer poDocId) {
        return getPaymentRequestByDocumentNumber(paymentRequestDao.getDocumentNumberByPaymentRequestId(poDocId));
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#getPaymentRequestsByPurchaseOrderId(java.lang.Integer)
     */
    @Override
    @NonTransactional
    public List<PaymentRequestDocument> getPaymentRequestsByPurchaseOrderId(Integer poDocId) {
        List<PaymentRequestDocument> preqs = new ArrayList<PaymentRequestDocument>();
        List<String> docNumbers = paymentRequestDao.getDocumentNumbersByPurchaseOrderId(poDocId);
        for (String docNumber : docNumbers) {
            PaymentRequestDocument preq = getPaymentRequestByDocumentNumber(docNumber);
            if (ObjectUtils.isNotNull(preq)) {
                preqs.add(preq);
            }
        }
        return preqs;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#getPaymentRequestsByStatusAndPurchaseOrderId(java.lang.String, java.lang.Integer)
     */
    @Override
    @NonTransactional
    public Map<String, String> getPaymentRequestsByStatusAndPurchaseOrderId(String applicationDocumentStatus,
            Integer purchaseOrderId) {
        List<String> paymentRequestDocNumbers = paymentRequestDao
                .getDocumentNumbersByPurchaseOrderId(purchaseOrderId);

        Map<String, String> paymentRequestResults = new HashMap<String, String>();
        paymentRequestResults.put("hasInProcess", "N");
        paymentRequestResults.put("checkInProcess", "N");

        //if there are no payment request document numbers exist then there is no need to
        //check for application document status on the workflow documents....
        if (paymentRequestDocNumbers == null || paymentRequestDocNumbers.isEmpty()) {
            return paymentRequestResults;
        }

        //helper method to filter the workflow documents that are created for Preq documents.
        //updates the map for hasInProcess value to Y if records found for app doc status
        //else sets the value of checkInProcess = Y.
        filterPaymentRequestByAppDocStatus(paymentRequestResults, paymentRequestDocNumbers,
                applicationDocumentStatus);

        return paymentRequestResults;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#getPaymentRequestsByPOIdInvoiceAmountInvoiceDate(java.lang.Integer,
     *      org.kuali.rice.core.api.util.type.KualiDecimal, java.sql.Date)
     */
    @Override
    @NonTransactional
    public List<PaymentRequestDocument> getPaymentRequestsByPOIdInvoiceAmountInvoiceDate(Integer poId,
            KualiDecimal invoiceAmount, Date invoiceDate) {
        LOG.debug("getPaymentRequestsByPOIdInvoiceAmountInvoiceDate() started");
        return paymentRequestDao.getActivePaymentRequestsByPOIdInvoiceAmountInvoiceDate(poId, invoiceAmount,
                invoiceDate);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#isInvoiceDateAfterToday(java.sql.Date)
     */
    @Override
    @NonTransactional
    public boolean isInvoiceDateAfterToday(Date invoiceDate) {
        // Check invoice date to make sure it is today or before
        Calendar now = Calendar.getInstance();
        now.set(Calendar.HOUR, 11);
        now.set(Calendar.MINUTE, 59);
        now.set(Calendar.SECOND, 59);
        now.set(Calendar.MILLISECOND, 59);
        Timestamp nowTime = new Timestamp(now.getTimeInMillis());
        Calendar invoiceDateC = Calendar.getInstance();
        invoiceDateC.setTime(invoiceDate);
        // set time to midnight
        invoiceDateC.set(Calendar.HOUR, 0);
        invoiceDateC.set(Calendar.MINUTE, 0);
        invoiceDateC.set(Calendar.SECOND, 0);
        invoiceDateC.set(Calendar.MILLISECOND, 0);
        Timestamp invoiceDateTime = new Timestamp(invoiceDateC.getTimeInMillis());
        return ((invoiceDateTime.compareTo(nowTime)) > 0);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#calculatePayDate(java.sql.Date,
     *      org.kuali.kfs.vnd.businessobject.PaymentTermType)
     */
    @Override
    @NonTransactional
    public java.sql.Date calculatePayDate(Date invoiceDate, PaymentTermType terms) {
        LOG.debug("calculatePayDate() started");
        // calculate the invoice + processed calendar
        Calendar invoicedDateCalendar = dateTimeService.getCalendar(invoiceDate);
        Calendar processedDateCalendar = dateTimeService.getCurrentCalendar();

        // add default number of days to processed
        String defaultDays = parameterService.getParameterValueAsString(PaymentRequestDocument.class,
                PurapParameterConstants.PURAP_PREQ_PAY_DATE_DEFAULT_NUMBER_OF_DAYS);
        processedDateCalendar.add(Calendar.DAY_OF_MONTH, Integer.parseInt(defaultDays));

        if (ObjectUtils.isNull(terms) || StringUtils.isEmpty(terms.getVendorPaymentTermsCode())) {
            invoicedDateCalendar.add(Calendar.DAY_OF_MONTH, PurapConstants.PREQ_PAY_DATE_EMPTY_TERMS_DEFAULT_DAYS);
            return returnLaterDate(invoicedDateCalendar, processedDateCalendar);
        }

        // Retrieve pay date variation parameter (currently defined as 2).  See parameter description for explanation of it's use.
        String payDateVariance = parameterService.getParameterValueAsString(PaymentRequestDocument.class,
                PurapParameterConstants.PURAP_PREQ_PAY_DATE_VARIANCE);
        Integer payDateVarianceInt = Integer.valueOf(payDateVariance);

        Integer discountDueNumber = terms.getVendorDiscountDueNumber();
        Integer netDueNumber = terms.getVendorNetDueNumber();
        if (ObjectUtils.isNotNull(discountDueNumber)) {
            // Decrease discount due number by the pay date variance
            discountDueNumber -= payDateVarianceInt;
            if (discountDueNumber < 0) {
                discountDueNumber = 0;
            }
            String discountDueTypeDescription = terms.getVendorDiscountDueTypeDescription();
            paymentTermsDateCalculation(discountDueTypeDescription, invoicedDateCalendar, discountDueNumber);
        } else if (ObjectUtils.isNotNull(netDueNumber)) {
            // Decrease net due number by the pay date variance
            netDueNumber -= payDateVarianceInt;
            if (netDueNumber < 0) {
                netDueNumber = 0;
            }
            String netDueTypeDescription = terms.getVendorNetDueTypeDescription();
            paymentTermsDateCalculation(netDueTypeDescription, invoicedDateCalendar, netDueNumber);
        } else {
            throw new RuntimeException("Neither discount or net number were specified for this payment terms type");
        }

        // return the later date
        return returnLaterDate(invoicedDateCalendar, processedDateCalendar);
    }

    /**
     * Returns whichever date is later, the invoicedDateCalendar or the processedDateCalendar.
     *
     * @param invoicedDateCalendar One of the dates to be used in determining which date is later.
     * @param processedDateCalendar The other date to be used in determining which date is later.
     * @return The date which is the later of the two given dates in the input parameters.
     */
    protected java.sql.Date returnLaterDate(Calendar invoicedDateCalendar, Calendar processedDateCalendar) {
        if (invoicedDateCalendar.after(processedDateCalendar)) {
            return new java.sql.Date(invoicedDateCalendar.getTimeInMillis());
        } else {
            return new java.sql.Date(processedDateCalendar.getTimeInMillis());
        }
    }

    /**
     * Calculates the paymentTermsDate given the dueTypeDescription, invoicedDateCalendar and the dueNumber.
     *
     * @param dueTypeDescription The due type description of the payment term.
     * @param invoicedDateCalendar The Calendar object of the invoice date.
     * @param discountDueNumber Either the vendorDiscountDueNumber or the vendorDiscountDueNumber of the payment term.
     */
    protected void paymentTermsDateCalculation(String dueTypeDescription, Calendar invoicedDateCalendar,
            Integer dueNumber) {

        if (StringUtils.equals(dueTypeDescription, PurapConstants.PREQ_PAY_DATE_DATE)) {
            // date specified set to date in next month
            invoicedDateCalendar.add(Calendar.MONTH, 1);
            invoicedDateCalendar.set(Calendar.DAY_OF_MONTH, dueNumber.intValue());
        } else if (StringUtils.equals(PurapConstants.PREQ_PAY_DATE_DAYS, dueTypeDescription)) {
            // days specified go forward that number
            invoicedDateCalendar.add(Calendar.DAY_OF_MONTH, dueNumber.intValue());
        } else {
            // improper string
            throw new RuntimeException(
                    "missing payment terms description or not properly enterred on payment term maintenance doc");
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#calculatePaymentRequest(org.kuali.kfs.module.purap.document.PaymentRequestDocument,
     *      boolean)
     */
    @Override
    @NonTransactional
    public void calculatePaymentRequest(PaymentRequestDocument paymentRequest, boolean updateDiscount) {
        LOG.debug("calculatePaymentRequest() started");

        // general calculation, i.e. for the whole preq document
        if (ObjectUtils.isNull(paymentRequest.getPaymentRequestPayDate())) {
            paymentRequest.setPaymentRequestPayDate(
                    calculatePayDate(paymentRequest.getInvoiceDate(), paymentRequest.getVendorPaymentTerms()));
        }

        distributeAccounting(paymentRequest);

        purapService.calculateTax(paymentRequest);

        // do proration for full order and trade in
        purapService.prorateForTradeInAndFullOrderDiscount(paymentRequest);

        // do proration for payment terms discount
        if (updateDiscount) {
            calculateDiscount(paymentRequest);
        }

        distributeAccounting(paymentRequest);
    }

    /**
     * Calculates the discount item for this paymentRequest.
     *
     * @param paymentRequestDocument The payment request document whose discount to be calculated.
     */
    protected void calculateDiscount(PaymentRequestDocument paymentRequestDocument) {
        PaymentRequestItem discountItem = findDiscountItem(paymentRequestDocument);
        // find out if we really need the discount item
        PaymentTermType pt = paymentRequestDocument.getVendorPaymentTerms();
        if ((pt != null) && (pt.getVendorPaymentTermsPercent() != null)
                && (BigDecimal.ZERO.compareTo(pt.getVendorPaymentTermsPercent()) != 0)) {
            if (discountItem == null) {
                // set discountItem and add to items
                // this is probably not the best way of doing it but should work for now if we start excluding discount from below
                // we will need to manually add
                purapService.addBelowLineItems(paymentRequestDocument);

                // fix up below the line items
                removeIneligibleAdditionalCharges(paymentRequestDocument);

                discountItem = findDiscountItem(paymentRequestDocument);
            }

            // Deleted the discountItem.getExtendedPrice() null and isZero
            PaymentRequestItem fullOrderItem = findFullOrderDiscountItem(paymentRequestDocument);
            KualiDecimal fullOrderAmount = KualiDecimal.ZERO;
            KualiDecimal fullOrderTaxAmount = KualiDecimal.ZERO;

            if (fullOrderItem != null) {
                fullOrderAmount = (ObjectUtils.isNotNull(fullOrderItem.getExtendedPrice()))
                        ? fullOrderItem.getExtendedPrice()
                        : KualiDecimal.ZERO;
                fullOrderTaxAmount = (ObjectUtils.isNotNull(fullOrderItem.getItemTaxAmount()))
                        ? fullOrderItem.getItemTaxAmount()
                        : KualiDecimal.ZERO;
            }
            KualiDecimal totalCost = paymentRequestDocument.getTotalPreTaxDollarAmountAboveLineItems()
                    .add(fullOrderAmount);
            PurApItem tradeInItem = paymentRequestDocument.getTradeInItem();
            if (ObjectUtils.isNotNull(tradeInItem)) {
                totalCost = totalCost.add(tradeInItem.getTotalAmount());
            }
            BigDecimal discountAmount = pt.getVendorPaymentTermsPercent().multiply(totalCost.bigDecimalValue())
                    .multiply(new BigDecimal(PurapConstants.PREQ_DISCOUNT_MULT));

            // do we really need to set both, not positive, but probably won't hurt
            discountItem.setItemUnitPrice(discountAmount.setScale(2, KualiDecimal.ROUND_BEHAVIOR));
            discountItem.setExtendedPrice(new KualiDecimal(discountAmount));

            // set tax amount
            boolean salesTaxInd = parameterService.getParameterValueAsBoolean(
                    KfsParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.ENABLE_SALES_TAX_IND);
            boolean useTaxIndicator = paymentRequestDocument.isUseTaxIndicator();

            if (salesTaxInd == true && useTaxIndicator == false) {
                KualiDecimal totalTax = paymentRequestDocument.getTotalTaxAmountAboveLineItems()
                        .add(fullOrderTaxAmount);
                BigDecimal discountTaxAmount = null;
                if (totalCost.isNonZero()) {
                    discountTaxAmount = discountAmount.divide(totalCost.bigDecimalValue())
                            .multiply(totalTax.bigDecimalValue());
                } else {
                    discountTaxAmount = BigDecimal.ZERO;
                }

                discountItem.setItemTaxAmount(new KualiDecimal(
                        discountTaxAmount.setScale(KualiDecimal.SCALE, KualiDecimal.ROUND_BEHAVIOR)));
            }

            // set document
            discountItem.setPurapDocument(paymentRequestDocument);
        } else { // no discount
            if (discountItem != null) {
                paymentRequestDocument.getItems().remove(discountItem);
            }
        }

    }

    @Override
    @NonTransactional
    public void clearTax(PaymentRequestDocument document) {
        // remove all existing tax items added by previous calculation
        removeTaxItems(document);
        // reset values
        document.setTaxClassificationCode(null);
        document.setTaxFederalPercent(null);
        document.setTaxStatePercent(null);
        document.setTaxCountryCode(null);
        document.setTaxNQIId(null);

        document.setTaxForeignSourceIndicator(false);
        document.setTaxExemptTreatyIndicator(false);
        document.setTaxOtherExemptIndicator(false);
        document.setTaxGrossUpIndicator(false);
        document.setTaxUSAIDPerDiemIndicator(false);
        document.setTaxSpecialW4Amount(null);

    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#calculateTaxArea(org.kuali.kfs.module.purap.document.PaymentRequestDocument)
     */
    @Override
    @NonTransactional
    public void calculateTaxArea(PaymentRequestDocument preq) {
        LOG.debug("calculateTaxArea() started");

        // remove all existing tax items added by previous calculation
        removeTaxItems(preq);

        // don't need to calculate tax items if TaxClassificationCode is N (Non_Reportable)
        if (StringUtils.equalsIgnoreCase(preq.getTaxClassificationCode(), "N")) {
            return;
        }

        // reserve the grand total excluding any tax amount, to be used as the base to compute all tax items
        // if we don't reserve this, the pre-tax total could be changed as new tax items are added
        BigDecimal taxableAmount = preq.getGrandPreTaxTotal().bigDecimalValue();

        // generate and add state tax gross up item and its accounting line, update total amount,
        // if gross up indicator is true and tax rate is non-zero
        if (preq.getTaxGrossUpIndicator() && preq.getTaxStatePercent().compareTo(new BigDecimal(0)) != 0) {
            PurApItem stateGrossItem = addTaxItem(preq, ItemTypeCodes.ITEM_TYPE_STATE_GROSS_CODE, taxableAmount);
        }

        // generate and add state tax item and its accounting line, update total amount, if tax rate is non-zero
        if (preq.getTaxStatePercent().compareTo(new BigDecimal(0)) != 0) {
            PurApItem stateTaxItem = addTaxItem(preq, ItemTypeCodes.ITEM_TYPE_STATE_TAX_CODE, taxableAmount);
        }

        // generate and add federal tax gross up item and its accounting line, update total amount,
        // if gross up indicator is true and tax rate is non-zero
        if (preq.getTaxGrossUpIndicator() && preq.getTaxFederalPercent().compareTo(new BigDecimal(0)) != 0) {
            PurApItem federalGrossItem = addTaxItem(preq, ItemTypeCodes.ITEM_TYPE_FEDERAL_GROSS_CODE,
                    taxableAmount);
        }

        // generate and add federal tax item and its accounting line, update total amount, if tax rate is non-zero
        if (preq.getTaxFederalPercent().compareTo(new BigDecimal(0)) != 0) {
            PurApItem federalTaxItem = addTaxItem(preq, ItemTypeCodes.ITEM_TYPE_FEDERAL_TAX_CODE, taxableAmount);
        }

        // FIXME if user request to add zero tax lines and remove them after tax approval,
        // then remove the conditions above when adding the tax lines, and
        // add a branch in PaymentRequestDocument.processNodeChange to call PurapService.deleteUnenteredItems
    }

    /**
     * Removes all existing NRA tax items from the specified payment request.
     *
     * @param preq The payment request from which all tax items are to be removed.
     */
    protected void removeTaxItems(PaymentRequestDocument preq) {
        List<PurApItem> items = preq.getItems();
        for (int i = 0; i < items.size(); i++) {
            PurApItem item = items.get(i);
            String code = item.getItemTypeCode();
            if (ItemTypeCodes.ITEM_TYPE_FEDERAL_TAX_CODE.equals(code)
                    || ItemTypeCodes.ITEM_TYPE_STATE_TAX_CODE.equals(code)
                    || ItemTypeCodes.ITEM_TYPE_FEDERAL_GROSS_CODE.equals(code)
                    || ItemTypeCodes.ITEM_TYPE_STATE_GROSS_CODE.equals(code)) {
                items.remove(i--);
            }
        }
    }

    /**
     * Generates a NRA tax item and adds to the specified payment request, according to the specified item type code.
     *
     * @param preq The payment request the tax item will be added to.
     * @param itemTypeCode The item type code for the tax item.
     * @param taxableAmount The amount to which tax is computed against.
     * @return A fully populated PurApItem instance representing NRA tax amount data for the specified payment request.
     */
    protected PurApItem addTaxItem(PaymentRequestDocument preq, String itemTypeCode, BigDecimal taxableAmount) {
        PurApItem taxItem = null;

        try {
            taxItem = (PurApItem) preq.getItemClass().newInstance();
        } catch (IllegalAccessException e) {
            throw new InfrastructureException("Unable to access itemClass", e);
        } catch (InstantiationException e) {
            throw new InfrastructureException("Unable to instantiate itemClass", e);
        }

        // add item to preq before adding the accounting line
        taxItem.setItemTypeCode(itemTypeCode);
        preq.addItem(taxItem);

        // generate and add tax accounting line
        PurApAccountingLine taxLine = addTaxAccountingLine(taxItem, taxableAmount);

        // set extended price amount as now it's calculated when accounting line is generated
        taxItem.setItemUnitPrice(taxLine.getAmount().bigDecimalValue());
        taxItem.setExtendedPrice(taxLine.getAmount());

        // use item type description as the item description
        ItemType itemType = new ItemType();
        itemType.setItemTypeCode(itemTypeCode);
        itemType = (ItemType) businessObjectService.retrieve(itemType);
        taxItem.setItemType(itemType);
        taxItem.setItemDescription(itemType.getItemTypeDescription());

        return taxItem;
    }

    /**
     * Generates a PurAP accounting line and adds to the specified tax item.
     *
     * @param taxItem The specified tax item the accounting line will be associated with.
     * @param taxableAmount The amount to which tax is computed against.
     * @return A fully populated PurApAccountingLine instance for the specified tax item.
     */
    protected PurApAccountingLine addTaxAccountingLine(PurApItem taxItem, BigDecimal taxableAmount) {
        PaymentRequestDocument preq = taxItem.getPurapDocument();
        PurApAccountingLine taxLine = null;

        try {
            taxLine = (PurApAccountingLine) taxItem.getAccountingLineClass().newInstance();
        } catch (IllegalAccessException e) {
            throw new InfrastructureException("Unable to access sourceAccountingLineClass", e);
        } catch (InstantiationException e) {
            throw new InfrastructureException("Unable to instantiate sourceAccountingLineClass", e);
        }

        // tax item type indicators
        boolean isFederalTax = ItemTypeCodes.ITEM_TYPE_FEDERAL_TAX_CODE.equals(taxItem.getItemTypeCode());
        boolean isFederalGross = ItemTypeCodes.ITEM_TYPE_FEDERAL_GROSS_CODE.equals(taxItem.getItemTypeCode());
        boolean isStateTax = ItemTypeCodes.ITEM_TYPE_STATE_TAX_CODE.equals(taxItem.getItemTypeCode());
        boolean isStateGross = ItemTypeCodes.ITEM_TYPE_STATE_GROSS_CODE.equals(taxItem.getItemTypeCode());
        boolean isFederal = isFederalTax || isFederalGross; // true for federal tax/gross; false for state tax/gross
        boolean isGross = isFederalGross || isStateGross; // true for federal/state gross, false for federal/state tax

        // obtain accounting line info according to tax item type code
        String taxChart = null;
        String taxAccount = null;
        String taxObjectCode = null;

        if (isGross) {
            // for gross up tax items, use preq's first item's first accounting line, which shall exist at this point
            AccountingLine line1 = preq.getFirstAccount();
            taxChart = line1.getChartOfAccountsCode();
            taxAccount = line1.getAccountNumber();
            taxObjectCode = line1.getFinancialObjectCode();
        } else if (isFederalTax) {
            // for federal tax item, get chart, account, object code info from parameters
            taxChart = parameterService.getParameterValueAsString(PaymentRequestDocument.class,
                    NRATaxParameters.FEDERAL_TAX_PARM_PREFIX + NRATaxParameters.TAX_PARM_CHART_SUFFIX);
            taxAccount = parameterService.getParameterValueAsString(PaymentRequestDocument.class,
                    NRATaxParameters.FEDERAL_TAX_PARM_PREFIX + NRATaxParameters.TAX_PARM_ACCOUNT_SUFFIX);
            taxObjectCode = parameterService.getSubParameterValueAsString(PaymentRequestDocument.class,
                    NRATaxParameters.FEDERAL_TAX_PARM_PREFIX
                            + NRATaxParameters.TAX_PARM_OBJECT_BY_INCOME_CLASS_SUFFIX,
                    preq.getTaxClassificationCode());
            if (StringUtils.isBlank(taxChart) || StringUtils.isBlank(taxAccount)
                    || StringUtils.isBlank(taxObjectCode)) {
                LOG.error("Unable to retrieve federal tax parameters.");
                throw new RuntimeException("Unable to retrieve federal tax parameters.");
            }
        } else if (isStateTax) {
            // for state tax item, get chart, account, object code info from parameters
            taxChart = parameterService.getParameterValueAsString(PaymentRequestDocument.class,
                    NRATaxParameters.STATE_TAX_PARM_PREFIX + NRATaxParameters.TAX_PARM_CHART_SUFFIX);
            taxAccount = parameterService.getParameterValueAsString(PaymentRequestDocument.class,
                    NRATaxParameters.STATE_TAX_PARM_PREFIX + NRATaxParameters.TAX_PARM_ACCOUNT_SUFFIX);
            taxObjectCode = parameterService.getSubParameterValueAsString(PaymentRequestDocument.class,
                    NRATaxParameters.STATE_TAX_PARM_PREFIX
                            + NRATaxParameters.TAX_PARM_OBJECT_BY_INCOME_CLASS_SUFFIX,
                    preq.getTaxClassificationCode());
            if (StringUtils.isBlank(taxChart) || StringUtils.isBlank(taxAccount)
                    || StringUtils.isBlank(taxObjectCode)) {
                LOG.error("Unable to retrieve state tax parameters.");
                throw new RuntimeException("Unable to retrieve state tax parameters.");
            }
        }

        // calculate tax amount according to gross up indicator and federal/state tax type
        /*
         * The formula of tax and gross up amount are as follows: if (not gross up) gross not existing taxFederal/State = - amount *
         * rateFederal/State otherwise gross up grossFederal/State = amount * rateFederal/State / (1 - rateFederal - rateState) tax
         * = - gross
         */

        // pick federal/state tax rate
        BigDecimal taxPercentFederal = preq.getTaxFederalPercent();
        BigDecimal taxPercentState = preq.getTaxStatePercent();
        BigDecimal taxPercent = isFederal ? taxPercentFederal : taxPercentState;

        // divider value according to gross up or not
        BigDecimal taxDivider = new BigDecimal(100);
        if (preq.getTaxGrossUpIndicator()) {
            taxDivider = taxDivider.subtract(taxPercentFederal.add(taxPercentState));
        }

        // tax = amount * rate / divider
        BigDecimal taxAmount = taxableAmount.multiply(taxPercent);
        taxAmount = taxAmount.divide(taxDivider, 5, BigDecimal.ROUND_HALF_UP);

        // tax is always negative, since it reduces the total amount; while gross up is always the positive of tax
        if (!isGross) {
            taxAmount = taxAmount.negate();
        }

        // populate necessary accounting line fields
        taxLine.setDocumentNumber(preq.getDocumentNumber());
        taxLine.setSequenceNumber(preq.getNextSourceLineNumber());
        taxLine.setChartOfAccountsCode(taxChart);
        taxLine.setAccountNumber(taxAccount);
        taxLine.setFinancialObjectCode(taxObjectCode);
        taxLine.setAmount(new KualiDecimal(taxAmount));

        // add the accounting line to the item
        taxLine.setItemIdentifier(taxItem.getItemIdentifier());
        taxLine.setPurapItem(taxItem);
        taxItem.getSourceAccountingLines().add(taxLine);

        return taxLine;
    }

    /**
     * Finds the discount item of the payment request document.
     *
     * @param paymentRequestDocument The payment request document to be used to find the discount item.
     * @return The discount item if it exists.
     */
    protected PaymentRequestItem findDiscountItem(PaymentRequestDocument paymentRequestDocument) {
        PaymentRequestItem discountItem = null;
        for (PaymentRequestItem preqItem : (List<PaymentRequestItem>) paymentRequestDocument.getItems()) {
            if (StringUtils.equals(preqItem.getItemTypeCode(),
                    PurapConstants.ItemTypeCodes.ITEM_TYPE_PMT_TERMS_DISCOUNT_CODE)) {
                discountItem = preqItem;
                break;
            }
        }
        return discountItem;
    }

    /**
     * Finds the full order discount item of the payment request document.
     *
     * @param paymentRequestDocument The payment request document to be used to find the full order discount item.
     * @return The discount item if it exists.
     */
    protected PaymentRequestItem findFullOrderDiscountItem(PaymentRequestDocument paymentRequestDocument) {
        PaymentRequestItem discountItem = null;
        for (PaymentRequestItem preqItem : (List<PaymentRequestItem>) paymentRequestDocument.getItems()) {
            if (StringUtils.equals(preqItem.getItemTypeCode(),
                    PurapConstants.ItemTypeCodes.ITEM_TYPE_ORDER_DISCOUNT_CODE)) {
                discountItem = preqItem;
                break;
            }
        }
        return discountItem;
    }

    /**
     * Distributes accounts for a payment request document.
     *
     * @param paymentRequestDocument
     */
    protected void distributeAccounting(PaymentRequestDocument paymentRequestDocument) {
        // update the account amounts before doing any distribution
        purapAccountingService.updateAccountAmounts(paymentRequestDocument);

        String accountDistributionMethod = paymentRequestDocument.getAccountDistributionMethod();

        for (PaymentRequestItem item : (List<PaymentRequestItem>) paymentRequestDocument.getItems()) {
            KualiDecimal totalAmount = KualiDecimal.ZERO;
            List<PurApAccountingLine> distributedAccounts = null;
            List<SourceAccountingLine> summaryAccounts = null;
            Set excludedItemTypeCodes = new HashSet();
            excludedItemTypeCodes.add(PurapConstants.ItemTypeCodes.ITEM_TYPE_PMT_TERMS_DISCOUNT_CODE);

            // skip above the line
            if (item.getItemType().isLineItemIndicator()) {
                continue;
            }

            if ((item.getSourceAccountingLines().isEmpty()) && (ObjectUtils.isNotNull(item.getExtendedPrice()))
                    && (KualiDecimal.ZERO.compareTo(item.getExtendedPrice()) != 0)) {
                if ((StringUtils.equals(PurapConstants.ItemTypeCodes.ITEM_TYPE_PMT_TERMS_DISCOUNT_CODE,
                        item.getItemType().getItemTypeCode())) && (paymentRequestDocument.getGrandTotal() != null)
                        && ((KualiDecimal.ZERO.compareTo(paymentRequestDocument.getGrandTotal()) != 0))) {

                    // No discount is applied to other item types other than item line
                    // See KFSMI-5210 for details

                    // total amount should be the line item total, not the grand total
                    totalAmount = paymentRequestDocument.getLineItemTotal();

                    // prorate item line accounts only
                    Set includedItemTypeCodes = new HashSet();
                    includedItemTypeCodes.add(PurapConstants.ItemTypeCodes.ITEM_TYPE_ITEM_CODE);
                    includedItemTypeCodes.add(PurapConstants.ItemTypeCodes.ITEM_TYPE_SERVICE_CODE);

                    summaryAccounts = purapAccountingService.generateSummaryIncludeItemTypesAndNoZeroTotals(
                            paymentRequestDocument.getItems(), includedItemTypeCodes);
                    //if summaryAccount is empty then do not call generateAccountDistributionForProration as
                    //there is a check in that method to throw NPE if accounts percents == 0..
                    //KFSMI-8487
                    if (summaryAccounts != null) {
                        distributedAccounts = purapAccountingService.generateAccountDistributionForProration(
                                summaryAccounts, totalAmount, PurapConstants.PRORATION_SCALE,
                                PaymentRequestAccount.class);
                    }
                    if (PurapConstants.AccountDistributionMethodCodes.SEQUENTIAL_CODE
                            .equalsIgnoreCase(accountDistributionMethod)) {
                        purapAccountingService.updatePreqAccountAmountsWithTotal(distributedAccounts,
                                item.getTotalAmount());

                    } else {

                        boolean rulePassed = true;
                        // check any business rules
                        rulePassed &= kualiRuleService.applyRules(
                                new PurchasingAccountsPayableItemPreCalculateEvent(paymentRequestDocument, item));

                        if (rulePassed) {
                            purapAccountingService.updatePreqProporationalAccountAmountsWithTotal(
                                    distributedAccounts, item.getTotalAmount());
                        }
                    }
                } else {
                    PurchaseOrderItem poi = item.getPurchaseOrderItem();
                    if ((poi != null) && (poi.getSourceAccountingLines() != null)
                            && (!(poi.getSourceAccountingLines().isEmpty())) && (poi.getExtendedPrice() != null)
                            && ((KualiDecimal.ZERO.compareTo(poi.getExtendedPrice())) != 0)) {
                        // use accounts from purchase order item matching this item
                        // account list of current item is already empty
                        item.generateAccountListFromPoItemAccounts(poi.getSourceAccountingLines());
                    } else {
                        totalAmount = paymentRequestDocument.getPurchaseOrderDocument()
                                .getTotalDollarAmountAboveLineItems();
                        purapAccountingService
                                .updateAccountAmounts(paymentRequestDocument.getPurchaseOrderDocument());
                        summaryAccounts = purapAccountingService.generateSummary(PurApItemUtils
                                .getAboveTheLineOnly(paymentRequestDocument.getPurchaseOrderDocument().getItems()));
                        //if summaryAccount is empty then do not call generateAccountDistributionForProration as
                        //there is a check in that method to throw NPE if accounts percents == 0..
                        //KFSMI-8487
                        if (summaryAccounts != null) {
                            distributedAccounts = purapAccountingService.generateAccountDistributionForProration(
                                    summaryAccounts, totalAmount, new Integer("6"), PaymentRequestAccount.class);
                        }
                    }
                }
                if (CollectionUtils.isNotEmpty(distributedAccounts)
                        && CollectionUtils.isEmpty(item.getSourceAccountingLines())) {
                    item.setSourceAccountingLines(distributedAccounts);
                }
            }
        }

        // update again now that distribute is finished. (Note: we may not need this anymore now that I added updateItem line above
        //leave the call below since we need to this when sequential method is used on the document.
        purapAccountingService.updateAccountAmounts(paymentRequestDocument);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#addHoldOnPaymentRequest(org.kuali.kfs.module.purap.document.PaymentRequestDocument,
     *      java.lang.String)
     */
    @Override
    @NonTransactional
    public PaymentRequestDocument addHoldOnPaymentRequest(PaymentRequestDocument document, String note)
            throws Exception {
        // save the note
        Note noteObj = documentService.createNoteFromDocument(document, note);
        document.addNote(noteObj);
        noteService.save(noteObj);

        document.setHoldIndicator(true);
        document.setLastActionPerformedByPersonId(GlobalVariables.getUserSession().getPerson().getPrincipalId());
        purapService.saveDocumentNoValidation(document);

        return document;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#removeHoldOnPaymentRequest(org.kuali.kfs.module.purap.document.PaymentRequestDocument)
     */
    @Override
    @NonTransactional
    public PaymentRequestDocument removeHoldOnPaymentRequest(PaymentRequestDocument document, String note)
            throws Exception {
        // save the note
        Note noteObj = documentService.createNoteFromDocument(document, note);
        document.addNote(noteObj);
        noteService.save(noteObj);

        document.setHoldIndicator(false);
        document.setLastActionPerformedByPersonId(null);
        purapService.saveDocumentNoValidation(document);

        return document;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#addHoldOnPaymentRequest(org.kuali.kfs.module.purap.document.PaymentRequestDocument,
     *      java.lang.String)
     */
    @Override
    @NonTransactional
    public void requestCancelOnPaymentRequest(PaymentRequestDocument document, String note) throws Exception {
        // save the note
        Note noteObj = documentService.createNoteFromDocument(document, note);
        document.addNote(noteObj);
        noteService.save(noteObj);

        document.setPaymentRequestedCancelIndicator(true);
        document.setLastActionPerformedByPersonId(GlobalVariables.getUserSession().getPerson().getPrincipalId());
        document.setAccountsPayableRequestCancelIdentifier(
                GlobalVariables.getUserSession().getPerson().getPrincipalId());
        purapService.saveDocumentNoValidation(document);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#removeHoldOnPaymentRequest(org.kuali.kfs.module.purap.document.PaymentRequestDocument)
     */
    @Override
    @NonTransactional
    public void removeRequestCancelOnPaymentRequest(PaymentRequestDocument document, String note) throws Exception {
        // save the note
        Note noteObj = documentService.createNoteFromDocument(document, note);
        document.addNote(noteObj);
        noteService.save(noteObj);

        clearRequestCancelFields(document);

        purapService.saveDocumentNoValidation(document);

    }

    /**
     * Clears the request cancel fields.
     *
     * @param document The payment request document whose request cancel fields to be cleared.
     */
    protected void clearRequestCancelFields(PaymentRequestDocument document) {
        document.setPaymentRequestedCancelIndicator(false);
        document.setLastActionPerformedByPersonId(null);
        document.setAccountsPayableRequestCancelIdentifier(null);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#isExtracted(org.kuali.kfs.module.purap.document.PaymentRequestDocument)
     */
    @Override
    @NonTransactional
    public boolean isExtracted(PaymentRequestDocument document) {
        return (ObjectUtils.isNull(document.getExtractedTimestamp()) ? false : true);
    }

    protected boolean isBeingAdHocRouted(PaymentRequestDocument document) {
        return financialSystemWorkflowHelperService.isAdhocApprovalRequestedForPrincipal(
                document.getDocumentHeader().getWorkflowDocument(),
                GlobalVariables.getUserSession().getPrincipalId());
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#cancelExtractedPaymentRequest(org.kuali.kfs.module.purap.document.PaymentRequestDocument,
     *      java.lang.String)
     */
    @Override
    @NonTransactional
    public void cancelExtractedPaymentRequest(PaymentRequestDocument paymentRequest, String note) {
        LOG.debug("cancelExtractedPaymentRequest() started");
        if (PaymentRequestStatuses.CANCELLED_STATUSES.contains(paymentRequest.getApplicationDocumentStatus())) {
            LOG.debug("cancelExtractedPaymentRequest() ended");
            return;
        }

        try {
            Note cancelNote = documentService.createNoteFromDocument(paymentRequest, note);
            paymentRequest.addNote(cancelNote);
            noteService.save(cancelNote);
        } catch (Exception e) {
            throw new RuntimeException(PurapConstants.REQ_UNABLE_TO_CREATE_NOTE, e);
        }

        // cancel extracted should not reopen PO
        paymentRequest.setReopenPurchaseOrderIndicator(false);

        getAccountsPayableService().cancelAccountsPayableDocument(paymentRequest, ""); // Performs save, so
        // no explicit save
        // is necessary
        if (LOG.isDebugEnabled()) {
            LOG.debug("cancelExtractedPaymentRequest() PREQ " + paymentRequest.getPurapDocumentIdentifier()
                    + " Cancelled Without Workflow");
            LOG.debug("cancelExtractedPaymentRequest() ended");
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#resetExtractedPaymentRequest(org.kuali.kfs.module.purap.document.PaymentRequestDocument,
     *      java.lang.String)
     */
    @Override
    @NonTransactional
    public void resetExtractedPaymentRequest(PaymentRequestDocument paymentRequest, String note) {
        LOG.debug("resetExtractedPaymentRequest() started");
        if (PaymentRequestStatuses.CANCELLED_STATUSES.contains(paymentRequest.getApplicationDocumentStatus())) {
            LOG.debug("resetExtractedPaymentRequest() ended");
            return;
        }
        paymentRequest.setExtractedTimestamp(null);
        paymentRequest.setPaymentPaidTimestamp(null);
        String noteText = "This Payment Request is being reset for extraction by PDP " + note;
        try {
            Note resetNote = documentService.createNoteFromDocument(paymentRequest, noteText);
            paymentRequest.addNote(resetNote);
            noteService.save(resetNote);
        } catch (Exception e) {
            throw new RuntimeException(PurapConstants.REQ_UNABLE_TO_CREATE_NOTE + " " + e);
        }
        purapService.saveDocumentNoValidation(paymentRequest);
        if (LOG.isDebugEnabled()) {
            LOG.debug("resetExtractedPaymentRequest() PREQ " + paymentRequest.getPurapDocumentIdentifier()
                    + " Reset from Extracted status");
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#populatePaymentRequest(org.kuali.kfs.module.purap.document.PaymentRequestDocument)
     */
    @Override
    @NonTransactional
    public void populatePaymentRequest(PaymentRequestDocument paymentRequestDocument) {

        PurchaseOrderDocument purchaseOrderDocument = paymentRequestDocument.getPurchaseOrderDocument();

        // make a call to search for expired/closed accounts
        HashMap<String, ExpiredOrClosedAccountEntry> expiredOrClosedAccountList = getAccountsPayableService()
                .getExpiredOrClosedAccountList(paymentRequestDocument);

        paymentRequestDocument.populatePaymentRequestFromPurchaseOrder(purchaseOrderDocument,
                expiredOrClosedAccountList);

        paymentRequestDocument.getDocumentHeader().setDocumentDescription(createPreqDocumentDescription(
                paymentRequestDocument.getPurchaseOrderIdentifier(), paymentRequestDocument.getVendorName()));

        // write a note for expired/closed accounts if any exist and add a message stating there were expired/closed accounts at the
        // top of the document
        getAccountsPayableService().generateExpiredOrClosedAccountNote(paymentRequestDocument,
                expiredOrClosedAccountList);

        // set indicator so a message is displayed for accounts that were replaced due to expired/closed status
        if (!expiredOrClosedAccountList.isEmpty()) {
            paymentRequestDocument.setContinuationAccountIndicator(true);
        }

        // add discount item
        calculateDiscount(paymentRequestDocument);
        // distribute accounts (i.e. proration)
        distributeAccounting(paymentRequestDocument);

        // set bank code to default bank code in the system parameter
        Bank defaultBank = bankService.getDefaultBankByDocType(paymentRequestDocument.getClass());
        if (defaultBank != null) {
            paymentRequestDocument.setBankCode(defaultBank.getBankCode());
            paymentRequestDocument.setBank(defaultBank);
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#createPreqDocumentDescription(java.lang.Integer,
     *      java.lang.String)
     */
    @Override
    @NonTransactional
    public String createPreqDocumentDescription(Integer purchaseOrderIdentifier, String vendorName) {
        StringBuffer descr = new StringBuffer("");
        descr.append("PO: ");
        descr.append(purchaseOrderIdentifier);
        descr.append(" Vendor: ");
        descr.append(StringUtils.trimToEmpty(vendorName));

        int noteTextMaxLength = dataDictionaryService
                .getAttributeMaxLength(DocumentHeader.class, KRADPropertyConstants.DOCUMENT_DESCRIPTION).intValue();
        if (noteTextMaxLength >= descr.length()) {
            return descr.toString();
        } else {
            return descr.toString().substring(0, noteTextMaxLength);
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#populateAndSavePaymentRequest(org.kuali.kfs.module.purap.document.PaymentRequestDocument)
     */
    @Override
    @NonTransactional
    public void populateAndSavePaymentRequest(PaymentRequestDocument preq) throws WorkflowException {
        try {
            preq.updateAndSaveAppDocStatus(PurapConstants.PaymentRequestStatuses.APPDOC_IN_PROCESS);
            documentService.saveDocument(preq, AttributedContinuePurapEvent.class);
        } catch (ValidationException ve) {
            preq.updateAndSaveAppDocStatus(PurapConstants.PaymentRequestStatuses.APPDOC_INITIATE);
        } catch (WorkflowException we) {
            preq.updateAndSaveAppDocStatus(PurapConstants.PaymentRequestStatuses.APPDOC_INITIATE);

            String errorMsg = "Error saving document # " + preq.getDocumentHeader().getDocumentNumber() + " "
                    + we.getMessage();
            LOG.error(errorMsg, we);
            throw new RuntimeException(errorMsg, we);
        }
    }

    /**
     * If the full document entry has been completed and the status of the related purchase order document is closed, return true,
     * otherwise return false.
     *
     * @param apDoc The AccountsPayableDocument to be determined whether its purchase order should be reversed.
     * @return boolean true if the purchase order should be reversed.
     * @see org.kuali.kfs.module.purap.document.service.AccountsPayableDocumentSpecificService#shouldPurchaseOrderBeReversed
     *      (org.kuali.kfs.module.purap.document.AccountsPayableDocument)
     */
    @Override
    @NonTransactional
    public boolean shouldPurchaseOrderBeReversed(AccountsPayableDocument apDoc) {
        PurchaseOrderDocument po = apDoc.getPurchaseOrderDocument();
        if (ObjectUtils.isNull(po)) {
            throw new RuntimeException("po should never be null on PREQ");
        }
        // if past full entry and already closed return true
        if (purapService.isFullDocumentEntryCompleted(apDoc) && StringUtils.equalsIgnoreCase(
                PurapConstants.PurchaseOrderStatuses.APPDOC_CLOSED, po.getApplicationDocumentStatus())) {
            return true;
        }
        return false;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.AccountsPayableDocumentSpecificService#getPersonForCancel(org.kuali.kfs.module.purap.document.AccountsPayableDocument)
     */
    @Override
    @NonTransactional
    public Person getPersonForCancel(AccountsPayableDocument apDoc) {
        PaymentRequestDocument preqDoc = (PaymentRequestDocument) apDoc;
        Person user = null;
        if (preqDoc.isPaymentRequestedCancelIndicator()) {
            user = preqDoc.getLastActionPerformedByUser();
        }
        return user;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.AccountsPayableDocumentSpecificService#takePurchaseOrderCancelAction(org.kuali.kfs.module.purap.document.AccountsPayableDocument)
     */
    @Override
    @NonTransactional
    public void takePurchaseOrderCancelAction(AccountsPayableDocument apDoc) {
        PaymentRequestDocument preqDocument = (PaymentRequestDocument) apDoc;
        if (preqDocument.isReopenPurchaseOrderIndicator()) {
            String docType = PurapConstants.PurchaseOrderDocTypes.PURCHASE_ORDER_REOPEN_DOCUMENT;
            purchaseOrderService.createAndRoutePotentialChangeDocument(
                    preqDocument.getPurchaseOrderDocument().getDocumentNumber(), docType,
                    "reopened by Credit Memo " + apDoc.getPurapDocumentIdentifier() + "cancel", new ArrayList(),
                    PurapConstants.PurchaseOrderStatuses.APPDOC_PENDING_REOPEN);
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.AccountsPayableDocumentSpecificService#updateStatusByNode(java.lang.String,
     *      org.kuali.kfs.module.purap.document.AccountsPayableDocument)
     */
    @Override
    @NonTransactional
    public String updateStatusByNode(String currentNodeName, AccountsPayableDocument apDoc) {
        return updateStatusByNode(currentNodeName, (PaymentRequestDocument) apDoc);
    }

    /**
     * Updates the status of the payment request document.
     *
     * @param currentNodeName The current node name.
     * @param preqDoc The payment request document whose status to be updated.
     * @return The canceled status code.
     */
    protected String updateStatusByNode(String currentNodeName, PaymentRequestDocument preqDoc) {
        // remove request cancel if necessary
        clearRequestCancelFields(preqDoc);

        // update the status on the document

        String cancelledStatus = "";
        if (StringUtils.isEmpty(currentNodeName)) {
            // if empty probably not coming from workflow
            cancelledStatus = PurapConstants.PaymentRequestStatuses.APPDOC_CANCELLED_POST_AP_APPROVE;
        } else {
            cancelledStatus = PurapConstants.PaymentRequestStatuses.getPaymentRequestAppDocDisapproveStatuses()
                    .get(currentNodeName);
        }

        if (StringUtils.isNotBlank(cancelledStatus)) {
            try {
                preqDoc.updateAndSaveAppDocStatus(cancelledStatus);
            } catch (WorkflowException we) {
                throw new RuntimeException(
                        "Unable to save the route status data for document: " + preqDoc.getDocumentNumber(), we);
            }
            purapService.saveDocumentNoValidation(preqDoc);
        } else {
            logAndThrowRuntimeException(
                    "No status found to set for document being disapproved in node '" + currentNodeName + "'");
        }
        return cancelledStatus;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#markPaid(org.kuali.kfs.module.purap.document.PaymentRequestDocument,
     *      java.sql.Date)
     */
    @Override
    @NonTransactional
    public void markPaid(PaymentRequestDocument pr, Date processDate) {
        LOG.debug("markPaid() started");

        pr.setPaymentPaidTimestamp(new Timestamp(processDate.getTime()));
        purapService.saveDocumentNoValidation(pr);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#hasDiscountItem(org.kuali.kfs.module.purap.document.PaymentRequestDocument)
     */
    @Override
    @NonTransactional
    public boolean hasDiscountItem(PaymentRequestDocument preq) {
        return ObjectUtils.isNotNull(findDiscountItem(preq));
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.AccountsPayableDocumentSpecificService#poItemEligibleForAp(org.kuali.kfs.module.purap.document.AccountsPayableDocument,
     *      org.kuali.kfs.module.purap.businessobject.PurchaseOrderItem)
     */
    @Override
    @NonTransactional
    public boolean poItemEligibleForAp(AccountsPayableDocument apDoc, PurchaseOrderItem poi) {
        if (ObjectUtils.isNull(poi)) {
            throw new RuntimeException(
                    "item null in purchaseOrderItemEligibleForPayment ... this should never happen");
        }
        // if the po item is not active... skip it
        if (!poi.isItemActiveIndicator()) {
            return false;
        }

        ItemType poiType = poi.getItemType();
        if (ObjectUtils.isNull(poiType)) {
            return false;
        }

        if (poiType.isQuantityBasedGeneralLedgerIndicator()) {
            if (poi.getItemQuantity().isGreaterThan(poi.getItemInvoicedTotalQuantity())) {
                return true;
            }
            return false;
        } else { // not quantity based
                 // As long as it contains a number (whether it's 0, negative or positive number), we'll
                 // have to return true. This is so that the OutstandingEncumberedAmount and the
                 // Original Amount from PO column would appear on the page for Trade In.
            if (poi.getItemOutstandingEncumberedAmount() != null) {
                return true;
            }
            return false;
        }
    }

    @Override
    @NonTransactional
    public void removeIneligibleAdditionalCharges(PaymentRequestDocument document) {

        List<PaymentRequestItem> itemsToRemove = new ArrayList<PaymentRequestItem>();

        for (PaymentRequestItem item : (List<PaymentRequestItem>) document.getItems()) {

            // if no extended price and its an order discount or trade in, remove
            if (ObjectUtils.isNull(item.getPurchaseOrderItemUnitPrice())
                    && (ItemTypeCodes.ITEM_TYPE_ORDER_DISCOUNT_CODE.equals(item.getItemTypeCode())
                            || ItemTypeCodes.ITEM_TYPE_TRADE_IN_CODE.equals(item.getItemTypeCode()))) {
                itemsToRemove.add(item);
                continue;
            }

            // if a payment terms discount exists but not set on teh doc, remove
            if (StringUtils.equals(item.getItemTypeCode(),
                    PurapConstants.ItemTypeCodes.ITEM_TYPE_PMT_TERMS_DISCOUNT_CODE)) {
                PaymentTermType pt = document.getVendorPaymentTerms();
                if ((pt != null) && (pt.getVendorPaymentTermsPercent() != null)
                        && (BigDecimal.ZERO.compareTo(pt.getVendorPaymentTermsPercent()) != 0)) {
                    // discount ok
                } else {
                    // remove discount
                    itemsToRemove.add(item);
                }
                continue;
            }

        }

        // remove items marked for removal
        for (PaymentRequestItem item : itemsToRemove) {
            document.getItems().remove(item);
        }
    }

    @Override
    @NonTransactional
    public void changeVendor(PaymentRequestDocument preq, Integer headerId, Integer detailId) {

        VendorDetail primaryVendor = vendorService.getVendorDetail(
                preq.getOriginalVendorHeaderGeneratedIdentifier(),
                preq.getOriginalVendorDetailAssignedIdentifier());

        if (primaryVendor == null) {
            LOG.error("useAlternateVendor() primaryVendorDetail from database for header id " + headerId
                    + " and detail id " + detailId + "is null");
            throw new PurError("AlternateVendor: VendorDetail from database for header id " + headerId
                    + " and detail id " + detailId + "is null");
        }

        // set vendor detail
        VendorDetail vd = vendorService.getVendorDetail(headerId, detailId);
        if (vd == null) {
            LOG.error("changeVendor() VendorDetail from database for header id " + headerId + " and detail id "
                    + detailId + "is null");
            throw new PurError("changeVendor: VendorDetail from database for header id " + headerId
                    + " and detail id " + detailId + "is null");
        }
        preq.setVendorDetail(vd);
        preq.setVendorName(vd.getVendorName());
        preq.setVendorNumber(vd.getVendorNumber());
        preq.setVendorHeaderGeneratedIdentifier(vd.getVendorHeaderGeneratedIdentifier());
        preq.setVendorDetailAssignedIdentifier(vd.getVendorDetailAssignedIdentifier());
        preq.setVendorPaymentTermsCode(vd.getVendorPaymentTermsCode());
        preq.setVendorShippingPaymentTermsCode(vd.getVendorShippingPaymentTermsCode());
        preq.setVendorShippingTitleCode(vd.getVendorShippingTitleCode());
        preq.refreshReferenceObject("vendorPaymentTerms");
        preq.refreshReferenceObject("vendorShippingPaymentTerms");

        // Set vendor address
        String deliveryCampus = preq.getPurchaseOrderDocument().getDeliveryCampusCode();
        VendorAddress va = vendorService.getVendorDefaultAddress(headerId, detailId,
                VendorConstants.AddressTypes.REMIT, deliveryCampus);
        if (va == null) {
            va = vendorService.getVendorDefaultAddress(headerId, detailId,
                    VendorConstants.AddressTypes.PURCHASE_ORDER, deliveryCampus);
        }
        if (va == null) {
            LOG.error("changeVendor() VendorAddress from database for header id " + headerId + " and detail id "
                    + detailId + "is null");
            throw new PurError("changeVendor  VendorAddress from database for header id " + headerId
                    + " and detail id " + detailId + "is null");
        }

        if (preq != null) {
            setVendorAddress(va, preq);
        } else {
            LOG.error("changeVendor(): Null link back to the Purchase Order.");
            throw new PurError("Null link back to the Purchase Order.");
        }

        // change document description
        preq.getDocumentHeader().setDocumentDescription(
                createPreqDocumentDescription(preq.getPurchaseOrderIdentifier(), preq.getVendorName()));
    }

    /**
     * Set the Vendor address of the given ID.
     *
     * @param addressID ID of the address to set
     * @param pr PaymentRequest to set in
     * @return New PaymentRequest to use
     */
    protected void setVendorAddress(VendorAddress va, PaymentRequestDocument preq) {

        if (va != null) {
            preq.setVendorAddressGeneratedIdentifier(va.getVendorAddressGeneratedIdentifier());
            preq.setVendorAddressInternationalProvinceName(va.getVendorAddressInternationalProvinceName());
            preq.setVendorLine1Address(va.getVendorLine1Address());
            preq.setVendorLine2Address(va.getVendorLine2Address());
            preq.setVendorCityName(va.getVendorCityName());
            preq.setVendorStateCode(va.getVendorStateCode());
            preq.setVendorPostalCode(va.getVendorZipCode());
            preq.setVendorCountryCode(va.getVendorCountryCode());
        }

    }

    /**
     * Records the specified error message into the Log file and throws a runtime exception.
     *
     * @param errorMessage the error message to be logged.
     */
    protected void logAndThrowRuntimeException(String errorMessage) {
        this.logAndThrowRuntimeException(errorMessage, null);
    }

    /**
     * Records the specified error message into the Log file and throws the specified runtime exception.
     *
     * @param errorMessage the specified error message.
     * @param e the specified runtime exception.
     */
    protected void logAndThrowRuntimeException(String errorMessage, Exception e) {
        if (ObjectUtils.isNotNull(e)) {
            LOG.error(errorMessage, e);
            throw new RuntimeException(errorMessage, e);
        } else {
            LOG.error(errorMessage);
            throw new RuntimeException(errorMessage);
        }
    }

    /**
     * The given document here actually needs to be a Payment Request.
     *
     * @see org.kuali.kfs.module.purap.document.service.AccountsPayableDocumentSpecificService#generateGLEntriesCreateAccountsPayableDocument(org.kuali.kfs.module.purap.document.AccountsPayableDocument)
     */
    @Override
    @NonTransactional
    public void generateGLEntriesCreateAccountsPayableDocument(AccountsPayableDocument apDocument) {
        PaymentRequestDocument paymentRequest = (PaymentRequestDocument) apDocument;
        // JHK: this is not being injected because it would cause a circular reference in the Spring definitions
        SpringContext.getBean(PurapGeneralLedgerService.class).generateEntriesCreatePaymentRequest(paymentRequest);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#hasActivePaymentRequestsForPurchaseOrder(java.lang.Integer)
     */
    @Override
    @NonTransactional
    public boolean hasActivePaymentRequestsForPurchaseOrder(Integer purchaseOrderIdentifier) {

        boolean hasActivePreqs = false;
        List<PaymentRequestDocument> preqs = paymentRequestDao
                .getActivePaymentRequestDocumentNumbersForPurchaseOrder(purchaseOrderIdentifier);
        WorkflowDocument workflowDocument = null;

        // docNumbers = paymentRequestDao.getActivePaymentRequestDocumentNumbersForPurchaseOrder(purchaseOrderIdentifier);
        //docNumbers = filterPaymentRequestByAppDocStatus(docNumbers, PaymentRequestStatuses.STATUSES_POTENTIALLY_ACTIVE);

        for (PaymentRequestDocument preq : preqs) {
            if (preq.getApplicationDocumentStatus().equals(PaymentRequestStatuses.STATUSES_POTENTIALLY_ACTIVE)) {
                try {
                    workflowDocument = workflowDocumentService.loadWorkflowDocument(preq.getDocumentNumber(),
                            GlobalVariables.getUserSession().getPerson());
                } catch (WorkflowException we) {
                    throw new RuntimeException(we);
                }
                // if the document is not in a non-active status then return true and stop evaluation
                if (!(workflowDocument.isCanceled() || workflowDocument.isException())) {
                    hasActivePreqs = true;
                    break;
                }
            }
        }
        return hasActivePreqs;
    }

    /**
     * This method was added as part of the move to rice20 as a way to get at application doc status. Since
     * this data has been moved back into KFS this function is no longer necessary.  The code will be removed
     * in the 6.0 release.
     */
    @Deprecated
    protected List<String> getPaymentRequestDocNumberForAutoApprove() {
        Date todayAtMidnight = dateTimeService.getCurrentSqlDateMidnight();
        return paymentRequestDao.getEligibleForAutoApproval(todayAtMidnight);
    }

    /**
     *  Filter the results by application doc status
     *
     * @param lookupDocNumbers
     * @param appDocStatus
     * @return a list of document numbers matching application document status based on
     */
    protected void filterPaymentRequestByAppDocStatus(Map<String, String> paymentRequestResults,
            List<String> lookupDocNumbers, String... applicationDocumentStatus) {
        List<String> paymentRequestDocNumbersInclude = new ArrayList<String>();
        List<String> paymentRequestDocNumbersExclude = new ArrayList<String>();

        for (String docId : lookupDocNumbers) {
            try {
                PaymentRequestDocument preq = (PaymentRequestDocument) documentService.getByDocumentHeaderId(docId);
                if (Arrays.asList(applicationDocumentStatus).contains(preq.getApplicationDocumentStatus())) {
                    paymentRequestDocNumbersInclude.add(docId);
                } else {
                    paymentRequestDocNumbersExclude.add(docId);
                }
            } catch (WorkflowException ex) {
                LOG.warn("Error retrieving doc for doc #" + docId + ". This shouldn't happen.", ex);
                throw new RuntimeException(ex.getMessage(), ex);
            }
        }

        if (!paymentRequestDocNumbersInclude.isEmpty()) {
            paymentRequestResults.put("hasInProcess", "Y");
        }

        if (!paymentRequestDocNumbersExclude.isEmpty()) {
            paymentRequestResults.put("checkInProcess", "Y");
        }
    }

    /**
     * Wrapper class to the filterPaymentRequestByAppDocStatus
     *
     * This class first extract the payment request document numbers from the Payment Request Collections,
     * then perform the filterPaymentRequestByAppDocStatus function.  Base on the filtered payment request
     * doc number, reconstruct the filtered Payment Request Collection
     *
     * @param paymentRequestDocuments
     * @param appDocStatus
     * @return
     */
    protected Collection<PaymentRequestDocument> filterPaymentRequestByAppDocStatus(
            Collection<PaymentRequestDocument> paymentRequestDocuments, String... appDocStatus) {
        Collection<PaymentRequestDocument> filteredPaymentRequestDocuments = new ArrayList<PaymentRequestDocument>();
        List status = Arrays.asList(appDocStatus);
        for (PaymentRequestDocument paymentRequest : paymentRequestDocuments) {
            long start = System.currentTimeMillis();
            if (status.contains(paymentRequest.getApplicationDocumentStatus())) {
                filteredPaymentRequestDocuments.add(paymentRequest);
            }
            if (LOG.isDebugEnabled()) {
                LOG.debug(System.currentTimeMillis() - start + "ms to check app doc status");
            }
        }

        return filteredPaymentRequestDocuments;
    }

    /**
     * Wrapper class to the filterPaymentRequestByAppDocStatus (Collection<PaymentRequestDocument>)
     *
     * This class first construct the Payment Request Collection from the iterator, and then process through
     * filterPaymentRequestByAppDocStatus
     *
     * @param paymentRequestDocuments
     * @param appDocStatus
     * @return
     */
    protected Iterator<PaymentRequestDocument> filterPaymentRequestByAppDocStatus(
            Iterator<PaymentRequestDocument> paymentRequestIterator, String... appDocStatus) {
        Collection<PaymentRequestDocument> paymentRequestDocuments = new ArrayList<PaymentRequestDocument>();
        for (; paymentRequestIterator.hasNext();) {
            paymentRequestDocuments.add(paymentRequestIterator.next());
        }

        return filterPaymentRequestByAppDocStatus(paymentRequestDocuments, appDocStatus).iterator();
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#processPaymentRequestInReceivingStatus()
     */
    @Override
    @NonTransactional
    public void processPaymentRequestInReceivingStatus() {
        List<PaymentRequestDocument> preqs = paymentRequestDao.getPaymentRequestInReceivingStatus();
        // docNumbers = filterPaymentRequestByAppDocStatus(docNumbers, PurapConstants.PaymentRequestStatuses.APPDOC_AWAITING_RECEIVING_REVIEW);

        List<PaymentRequestDocument> preqsAwaitingReceiving = new ArrayList<PaymentRequestDocument>();
        for (PaymentRequestDocument preq : preqs) {
            //PaymentRequestDocument preq = getPaymentRequestByDocumentNumber(docNumber);
            if (ObjectUtils.isNotNull(preq)) {
                preqsAwaitingReceiving.add(preq);
            }
        }
        //if (ObjectUtils.isNotNull(preqsAwaitingReceiving)) {
        for (PaymentRequestDocument preqDoc : preqsAwaitingReceiving) {
            if (preqDoc.isReceivingRequirementMet() && preqDoc.getApplicationDocumentStatus()
                    .equals(PaymentRequestStatuses.APPDOC_AWAITING_RECEIVING_REVIEW)) {
                try {
                    documentService.approveDocument(preqDoc, "Approved by Receiving Required PREQ job", null);
                } catch (WorkflowException e) {
                    LOG.error(
                            "processPaymentRequestInReceivingStatus() Error approving payment request document from awaiting receiving",
                            e);
                    throw new RuntimeException("Error approving payment request document from awaiting receiving",
                            e);
                }
            }
        }
        // }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PaymentRequestService#allowBackpost(org.kuali.kfs.module.purap.document.PaymentRequestDocument)
     */
    @Override
    @NonTransactional
    public boolean allowBackpost(PaymentRequestDocument paymentRequestDocument) {
        int allowBackpost = (Integer.parseInt(parameterService
                .getParameterValueAsString(PaymentRequestDocument.class, PurapRuleConstants.ALLOW_BACKPOST_DAYS)));

        Calendar today = dateTimeService.getCurrentCalendar();
        Integer currentFY = universityDateService.getCurrentUniversityDate().getUniversityFiscalYear();
        java.util.Date priorClosingDateTemp = universityDateService.getLastDateOfFiscalYear(currentFY - 1);
        Calendar priorClosingDate = Calendar.getInstance();
        priorClosingDate.setTime(priorClosingDateTemp);

        // adding 1 to set the date to midnight the day after backpost is allowed so that preqs allow backpost on the last day
        Calendar allowBackpostDate = Calendar.getInstance();
        allowBackpostDate.setTime(priorClosingDate.getTime());
        allowBackpostDate.add(Calendar.DATE, allowBackpost + 1);

        Calendar preqInvoiceDate = Calendar.getInstance();
        preqInvoiceDate.setTime(paymentRequestDocument.getInvoiceDate());

        // if today is after the closing date but before/equal to the allowed backpost date and the invoice date is for the
        // prior year, set the year to prior year
        if ((today.compareTo(priorClosingDate) > 0) && (today.compareTo(allowBackpostDate) <= 0)
                && (preqInvoiceDate.compareTo(priorClosingDate) <= 0)) {
            LOG.debug("allowBackpost() within range to allow backpost; posting entry to period 12 of previous FY");
            return true;
        }

        LOG.debug("allowBackpost() not within range to allow backpost; posting entry to current FY");
        return false;
    }

    @Override
    @NonTransactional
    public boolean isPurchaseOrderValidForPaymentRequestDocumentCreation(
            PaymentRequestDocument paymentRequestDocument, PurchaseOrderDocument po) {
        Integer POID = paymentRequestDocument.getPurchaseOrderIdentifier();
        boolean valid = true;

        PurchaseOrderDocument purchaseOrderDocument = paymentRequestDocument.getPurchaseOrderDocument();
        if (ObjectUtils.isNull(purchaseOrderDocument)) {
            GlobalVariables.getMessageMap().putError(PurapPropertyConstants.PURCHASE_ORDER_IDENTIFIER,
                    PurapKeyConstants.ERROR_PURCHASE_ORDER_NOT_EXIST);
            valid &= false;
        } else if (purchaseOrderDocument.isPendingActionIndicator()) {
            GlobalVariables.getMessageMap().putError(PurapPropertyConstants.PURCHASE_ORDER_IDENTIFIER,
                    PurapKeyConstants.ERROR_PURCHASE_PENDING_ACTION);
            valid &= false;
        } else if (!StringUtils.equals(purchaseOrderDocument.getApplicationDocumentStatus(),
                PurapConstants.PurchaseOrderStatuses.APPDOC_OPEN)) {
            GlobalVariables.getMessageMap().putError(PurapPropertyConstants.PURCHASE_ORDER_IDENTIFIER,
                    PurapKeyConstants.ERROR_PURCHASE_ORDER_NOT_OPEN);
            valid &= false;
            // if the PO is pending and it is not a Retransmit, we cannot generate a Payment Request for it
        } else {
            // Verify that there exists at least 1 item left to be invoiced
            // valid &= encumberedItemExistsForInvoicing(purchaseOrderDocument);
        }

        return valid;
    }

    @Override
    @NonTransactional
    public boolean encumberedItemExistsForInvoicing(PurchaseOrderDocument document) {
        boolean zeroDollar = true;
        GlobalVariables.getMessageMap().clearErrorPath();
        GlobalVariables.getMessageMap().addToErrorPath(KFSPropertyConstants.DOCUMENT);
        for (PurchaseOrderItem poi : (List<PurchaseOrderItem>) document.getItems()) {
            // Quantity-based items
            if (poi.getItemType().isLineItemIndicator()
                    && poi.getItemType().isQuantityBasedGeneralLedgerIndicator()) {
                KualiDecimal encumberedQuantity = poi.getItemOutstandingEncumberedQuantity() == null
                        ? KualiDecimal.ZERO
                        : poi.getItemOutstandingEncumberedQuantity();
                if (encumberedQuantity.compareTo(KualiDecimal.ZERO) == 1) {
                    zeroDollar = false;
                    break;
                }
            }
            // Service Items or Below-the-line Items
            else if (poi.getItemType().isAmountBasedGeneralLedgerIndicator()
                    || poi.getItemType().isAdditionalChargeIndicator()) {
                KualiDecimal encumberedAmount = poi.getItemOutstandingEncumberedAmount() == null ? KualiDecimal.ZERO
                        : poi.getItemOutstandingEncumberedAmount();
                if (encumberedAmount.compareTo(KualiDecimal.ZERO) == 1) {
                    zeroDollar = false;
                    break;
                }
            }
        }

        return !zeroDollar;
    }

    @NonTransactional
    public void setDateTimeService(DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    @NonTransactional
    public void setParameterService(ParameterService parameterService) {
        this.parameterService = parameterService;
    }

    @NonTransactional
    public void setConfigurationService(ConfigurationService configurationService) {
        this.configurationService = configurationService;
    }

    @NonTransactional
    public void setDocumentService(DocumentService documentService) {
        this.documentService = documentService;
    }

    @NonTransactional
    public void setNoteService(NoteService noteService) {
        this.noteService = noteService;
    }

    @NonTransactional
    public void setPurapService(PurapService purapService) {
        this.purapService = purapService;
    }

    @NonTransactional
    public void setPaymentRequestDao(PaymentRequestDao paymentRequestDao) {
        this.paymentRequestDao = paymentRequestDao;
    }

    @NonTransactional
    public void setNegativePaymentRequestApprovalLimitService(
            NegativePaymentRequestApprovalLimitService negativePaymentRequestApprovalLimitService) {
        this.negativePaymentRequestApprovalLimitService = negativePaymentRequestApprovalLimitService;
    }

    @NonTransactional
    public void setPurapAccountingService(PurapAccountingService purapAccountingService) {
        this.purapAccountingService = purapAccountingService;
    }

    @NonTransactional
    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    @NonTransactional
    public void setPurapWorkflowIntegrationService(
            PurApWorkflowIntegrationService purapWorkflowIntegrationService) {
        this.purapWorkflowIntegrationService = purapWorkflowIntegrationService;
    }

    @NonTransactional
    public void setWorkflowDocumentService(WorkflowDocumentService workflowDocumentService) {
        this.workflowDocumentService = workflowDocumentService;
    }

    @NonTransactional
    public void setAccountsPayableService(AccountsPayableService accountsPayableService) {
        this.accountsPayableService = accountsPayableService;
    }

    @NonTransactional
    public void setVendorService(VendorService vendorService) {
        this.vendorService = vendorService;
    }

    @NonTransactional
    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
        this.dataDictionaryService = dataDictionaryService;
    }

    @NonTransactional
    public void setUniversityDateService(UniversityDateService universityDateService) {
        this.universityDateService = universityDateService;
    }

    @NonTransactional
    public void setBankService(BankService bankService) {
        this.bankService = bankService;
    }

    @NonTransactional
    public void setPurchaseOrderService(PurchaseOrderService purchaseOrderService) {
        this.purchaseOrderService = purchaseOrderService;
    }

    @NonTransactional
    public void setFinancialSystemWorkflowHelperService(
            FinancialSystemWorkflowHelperService financialSystemWorkflowHelperService) {
        this.financialSystemWorkflowHelperService = financialSystemWorkflowHelperService;
    }

    @NonTransactional
    public void setKualiRuleService(KualiRuleService kualiRuleService) {
        this.kualiRuleService = kualiRuleService;
    }

    /**
     * Gets the accountsPayableService attribute.
     *
     * @return Returns the accountsPayableService
     */
    @NonTransactional
    public AccountsPayableService getAccountsPayableService() {
        return SpringContext.getBean(AccountsPayableService.class);
    }

    /**
     * @return Returns the personService.
     */
    protected PersonService getPersonService() {
        if (personService == null) {
            personService = SpringContext.getBean(PersonService.class);
        }
        return personService;
    }

}