/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 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 org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.Account;
import org.kuali.kfs.coa.businessobject.AccountDelegate;
import org.kuali.kfs.coa.service.AccountService;
import org.kuali.kfs.coreservice.api.parameter.Parameter;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.integration.purap.CapitalAssetSystem;
import org.kuali.kfs.kns.document.MaintenanceDocument;
import org.kuali.kfs.kns.maintenance.Maintainable;
import org.kuali.kfs.kns.service.DataDictionaryService;
import org.kuali.kfs.kns.util.KNSGlobalVariables;
import org.kuali.kfs.krad.bo.AdHocRoutePerson;
import org.kuali.kfs.krad.bo.AdHocRouteRecipient;
import org.kuali.kfs.krad.bo.Note;
import org.kuali.kfs.krad.datadictionary.exception.UnknownDocumentTypeException;
import org.kuali.kfs.krad.document.Document;
import org.kuali.kfs.krad.document.DocumentBase;
import org.kuali.kfs.krad.exception.ValidationException;
import org.kuali.kfs.krad.rules.rule.event.RouteDocumentEvent;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.service.DocumentService;
import org.kuali.kfs.krad.service.KRADServiceLocator;
import org.kuali.kfs.krad.service.KualiRuleService;
import org.kuali.kfs.krad.service.MailService;
import org.kuali.kfs.krad.service.MaintenanceDocumentService;
import org.kuali.kfs.krad.service.NoteService;
import org.kuali.kfs.krad.service.SequenceAccessorService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.MessageMap;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.purap.PurapConstants;
import org.kuali.kfs.module.purap.PurapConstants.PODocumentsStrings;
import org.kuali.kfs.module.purap.PurapConstants.POTransmissionMethods;
import org.kuali.kfs.module.purap.PurapConstants.PurchaseOrderDocTypes;
import org.kuali.kfs.module.purap.PurapConstants.PurchaseOrderStatuses;
import org.kuali.kfs.module.purap.PurapConstants.RequisitionSources;
import org.kuali.kfs.module.purap.PurapKeyConstants;
import org.kuali.kfs.module.purap.PurapParameterConstants;
import org.kuali.kfs.module.purap.PurapPropertyConstants;
import org.kuali.kfs.module.purap.PurapRuleConstants;
import org.kuali.kfs.module.purap.batch.AutoCloseRecurringOrdersStep;
import org.kuali.kfs.module.purap.businessobject.AutoClosePurchaseOrderView;
import org.kuali.kfs.module.purap.businessobject.ContractManagerAssignmentDetail;
import org.kuali.kfs.module.purap.businessobject.CreditMemoView;
import org.kuali.kfs.module.purap.businessobject.PaymentRequestView;
import org.kuali.kfs.module.purap.businessobject.PurApAccountingLine;
import org.kuali.kfs.module.purap.businessobject.PurApItem;
import org.kuali.kfs.module.purap.businessobject.PurchaseOrderCapitalAssetItem;
import org.kuali.kfs.module.purap.businessobject.PurchaseOrderCapitalAssetSystem;
import org.kuali.kfs.module.purap.businessobject.PurchaseOrderItem;
import org.kuali.kfs.module.purap.businessobject.PurchaseOrderQuoteStatus;
import org.kuali.kfs.module.purap.businessobject.PurchaseOrderVendorQuote;
import org.kuali.kfs.module.purap.businessobject.PurchasingCapitalAssetItem;
import org.kuali.kfs.module.purap.businessobject.ReceivingThreshold;
import org.kuali.kfs.module.purap.document.ContractManagerAssignmentDocument;
import org.kuali.kfs.module.purap.document.PurchaseOrderDocument;
import org.kuali.kfs.module.purap.document.PurchaseOrderSplitDocument;
import org.kuali.kfs.module.purap.document.PurchasingDocument;
import org.kuali.kfs.module.purap.document.RequisitionDocument;
import org.kuali.kfs.module.purap.document.dataaccess.PurchaseOrderDao;
import org.kuali.kfs.module.purap.document.service.B2BPurchaseOrderService;
import org.kuali.kfs.module.purap.document.service.LogicContainer;
import org.kuali.kfs.module.purap.document.service.PaymentRequestService;
import org.kuali.kfs.module.purap.document.service.PrintService;
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.service.RequisitionService;
import org.kuali.kfs.module.purap.util.PurApObjectUtils;
import org.kuali.kfs.module.purap.util.ThresholdHelper;
import org.kuali.kfs.module.purap.util.ThresholdHelper.ThresholdSummary;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.FinancialSystemTransactionalDocumentBase;
import org.kuali.kfs.sys.document.service.FinancialSystemDocumentService;
import org.kuali.kfs.sys.document.validation.event.AttributedRouteDocumentEvent;
import org.kuali.kfs.sys.document.validation.event.DocumentSystemSaveEvent;
import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
import org.kuali.kfs.vnd.VendorConstants;
import org.kuali.kfs.vnd.VendorConstants.AddressTypes;
import org.kuali.kfs.vnd.businessobject.CommodityCode;
import org.kuali.kfs.vnd.businessobject.VendorAddress;
import org.kuali.kfs.vnd.businessobject.VendorCommodityCode;
import org.kuali.kfs.vnd.businessobject.VendorDetail;
import org.kuali.kfs.vnd.businessobject.VendorPhoneNumber;
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.mail.MailMessage;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.KewApiConstants;
import org.kuali.rice.kew.api.KewApiServiceLocator;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kew.api.action.ActionRequestType;
import org.kuali.rice.kew.api.document.WorkflowDocumentService;
import org.kuali.rice.kew.api.document.attribute.DocumentAttributeIndexingQueue;
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.kim.api.services.KimApiServiceLocator;
import org.springframework.transaction.annotation.Transactional;

import java.io.ByteArrayOutputStream;
import java.sql.Timestamp;
import java.text.ParseException;
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;


@Transactional
public class PurchaseOrderServiceImpl implements PurchaseOrderService {
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PurchaseOrderServiceImpl.class);

    protected BusinessObjectService businessObjectService;
    protected DateTimeService dateTimeService;
    protected DocumentService documentService;
    protected NoteService noteService;
    protected PurapService purapService;
    protected PrintService printService;
    protected PurchaseOrderDao purchaseOrderDao;
    protected WorkflowDocumentService workflowDocumentService;
    protected ConfigurationService kualiConfigurationService;
    protected KualiRuleService kualiRuleService;
    protected VendorService vendorService;
    protected RequisitionService requisitionService;
    protected PurApWorkflowIntegrationService purapWorkflowIntegrationService;
    protected MaintenanceDocumentService maintenanceDocumentService;
    protected ParameterService parameterService;
    protected PersonService personService;
    protected MailService mailService;
    protected B2BPurchaseOrderService b2bPurchaseOrderService;
    protected DataDictionaryService dataDictionaryService;
    protected FinancialSystemDocumentService financialSystemDocumentService;


    @Override
    public boolean isPurchaseOrderOpenForProcessing(Integer poId) {
        return isPurchaseOrderOpenForProcessing(getCurrentPurchaseOrder(poId));
    }

    @Override
    public boolean isPurchaseOrderOpenForProcessing(PurchaseOrderDocument purchaseOrderDocument) {
        boolean can = PurchaseOrderStatuses.APPDOC_OPEN.equals(purchaseOrderDocument.getApplicationDocumentStatus());
        can = can && purchaseOrderDocument.isPurchaseOrderCurrentIndicator() && !purchaseOrderDocument.isPendingActionIndicator();
        // can't be any PREQ or CM that have not completed fullDocumentEntry
        if (can) {
            List<PaymentRequestView> preqViews = purchaseOrderDocument.getRelatedViews().getRelatedPaymentRequestViews();
            if (preqViews != null) {
                for (PaymentRequestView preqView : preqViews) {
                    if (!purapService.isPaymentRequestFullDocumentEntryCompleted(preqView.getApplicationDocumentStatus())) {
                        return false;
                    }
                }
            }
            List<CreditMemoView> cmViews = purchaseOrderDocument.getRelatedViews().getRelatedCreditMemoViews();
            if (cmViews != null) {
                for (CreditMemoView cmView : cmViews) {
                    if (!purapService.isVendorCreditMemoFullDocumentEntryCompleted(cmView.getApplicationDocumentStatus())) {
                        return false;
                    }
                }
            }
        }

        // passed all conditions; return true
        return can;
    }

    @Override
    public boolean isCommodityCodeRequiredOnPurchaseOrder() {
        boolean enableCommodityCode = parameterService.getParameterValueAsBoolean(KfsParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.ENABLE_COMMODITY_CODE_IND);
        if (!enableCommodityCode) {
            return false;
        } else {
            return parameterService.getParameterValueAsBoolean(PurchaseOrderDocument.class, PurapRuleConstants.ITEMS_REQUIRE_COMMODITY_CODE_IND);
        }
    }

    /**
     * Sets the error map to a new, empty error map before calling saveDocumentNoValidation to save the document.
     *
     * @param document The purchase order document to be saved.
     */
    protected void saveDocumentNoValidationUsingClearMessageMap(PurchaseOrderDocument document) {
        MessageMap errorHolder = GlobalVariables.getMessageMap();
        GlobalVariables.setMessageMap(new MessageMap());
        try {
            purapService.saveDocumentNoValidation(document);
        } finally {
            GlobalVariables.setMessageMap(errorHolder);
        }
    }

    /**
     * Calls the saveDocument method of documentService to save the document.
     *
     * @param document The document to be saved.
     */
    protected void saveDocumentStandardSave(PurchaseOrderDocument document) {
        try {
            documentService.saveDocument(document);
        } catch (WorkflowException we) {
            String errorMsg = "Workflow Error saving document # " + document.getDocumentHeader().getDocumentNumber() + " " + we.getMessage();
            LOG.error(errorMsg, we);
            throw new RuntimeException(errorMsg, we);
        }
    }

    @Override
    public PurchasingCapitalAssetItem createCamsItem(PurchasingDocument purDoc, PurApItem purapItem) {
        PurchasingCapitalAssetItem camsItem = new PurchaseOrderCapitalAssetItem();
        camsItem.setItemIdentifier(purapItem.getItemIdentifier());
        // If the system type is INDIVIDUAL then for each of the capital asset items, we need a system attached to it.
        if (purDoc.getCapitalAssetSystemTypeCode().equals(PurapConstants.CapitalAssetTabStrings.INDIVIDUAL_ASSETS)) {
            CapitalAssetSystem resultSystem = new PurchaseOrderCapitalAssetSystem();
            camsItem.setPurchasingCapitalAssetSystem(resultSystem);
        }
        camsItem.setPurchasingDocument(purDoc);

        return camsItem;
    }

    @Override
    public CapitalAssetSystem createCapitalAssetSystem() {
        CapitalAssetSystem resultSystem = new PurchaseOrderCapitalAssetSystem();
        return resultSystem;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#createAutomaticPurchaseOrderDocument(org.kuali.kfs.module.purap.document.RequisitionDocument)
     */
    @Override
    public void createAutomaticPurchaseOrderDocument(RequisitionDocument reqDocument) {
        String newSessionUserId = KFSConstants.SYSTEM_USER;
        try {
            LogicContainer logicToRun = new LogicContainer() {
                @Override
                public Object runLogic(Object[] objects) throws Exception {
                    RequisitionDocument doc = (RequisitionDocument) objects[0];
                    // update REQ data
                    doc.setPurchaseOrderAutomaticIndicator(Boolean.TRUE);
                    // create PO and populate with default data
                    PurchaseOrderDocument po = generatePurchaseOrderFromRequisition(doc);
                    po.setDefaultValuesForAPO();
                    // check for print transmission method.. if print is selected
                    // the doc status needs to be "Pending To Print"..
                    checkForPrintTransmission(po);
                    po.setContractManagerCode(PurapConstants.APO_CONTRACT_MANAGER);

                    documentService.routeDocument(po, null, null);

                    final DocumentAttributeIndexingQueue documentAttributeIndexingQueue = KewApiServiceLocator.getDocumentAttributeIndexingQueue();
                    documentAttributeIndexingQueue.indexDocument(po.getDocumentNumber());

                    return null;
                }
            };
            purapService.performLogicWithFakedUserSession(newSessionUserId, logicToRun, new Object[]{reqDocument});
        } catch (WorkflowException e) {
            String errorMsg = "Workflow Exception caught: " + e.getLocalizedMessage();
            LOG.error(errorMsg, e);
            throw new RuntimeException(errorMsg, e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * checks for print option and if chosen then sets the app doc status to Pending To Print.
     *
     * @param po
     */
    protected void checkForPrintTransmission(PurchaseOrderDocument po) throws WorkflowException {
        if (PurapConstants.POTransmissionMethods.PRINT.equals(po.getPurchaseOrderRetransmissionMethodCode())) {
            po.updateAndSaveAppDocStatus(PurapConstants.PurchaseOrderStatuses.APPDOC_PENDING_PRINT);
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#createPurchaseOrderDocument(org.kuali.kfs.module.purap.document.RequisitionDocument,
     * java.lang.String, java.lang.Integer)
     */
    @Override
    public PurchaseOrderDocument createPurchaseOrderDocument(RequisitionDocument reqDocument, String newSessionUserId, Integer contractManagerCode) {
        try {
            LogicContainer logicToRun = new LogicContainer() {
                @Override
                public Object runLogic(Object[] objects) throws Exception {
                    RequisitionDocument doc = (RequisitionDocument) objects[0];
                    PurchaseOrderDocument po = generatePurchaseOrderFromRequisition(doc);
                    Integer cmCode = (Integer) objects[1];
                    po.setContractManagerCode(cmCode);
                    purapService.saveDocumentNoValidation(po);
                    return po;
                }
            };
            return (PurchaseOrderDocument) purapService.performLogicWithFakedUserSession(newSessionUserId, logicToRun, new Object[]{reqDocument, contractManagerCode});
        } catch (WorkflowException e) {
            String errorMsg = "Workflow Exception caught: " + e.getLocalizedMessage();
            LOG.error(errorMsg, e);
            throw new RuntimeException(errorMsg, e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Create Purchase Order and populate with data from Requisition and other default data
     *
     * @param reqDocument The requisition document from which we create the purchase order document.
     * @return The purchase order document created by this method.
     * @throws WorkflowException
     */
    protected PurchaseOrderDocument generatePurchaseOrderFromRequisition(RequisitionDocument reqDocument) throws WorkflowException {
        PurchaseOrderDocument poDocument = null;
        poDocument = (PurchaseOrderDocument) documentService.getNewDocument(PurchaseOrderDocTypes.PURCHASE_ORDER_DOCUMENT);
        poDocument.populatePurchaseOrderFromRequisition(reqDocument);

        poDocument.updateAndSaveAppDocStatus(PurapConstants.PurchaseOrderStatuses.APPDOC_IN_PROCESS);

        poDocument.setPurchaseOrderCurrentIndicator(true);
        poDocument.setPendingActionIndicator(false);

        if (RequisitionSources.B2B.equals(poDocument.getRequisitionSourceCode())) {
            String paramName = PurapParameterConstants.DEFAULT_B2B_VENDOR_CHOICE;
            String paramValue = parameterService.getParameterValueAsString(PurchaseOrderDocument.class, paramName);
            poDocument.setPurchaseOrderVendorChoiceCode(paramValue);
        }

        if (ObjectUtils.isNotNull(poDocument.getVendorContract())) {
            poDocument.setVendorPaymentTermsCode(poDocument.getVendorContract().getVendorPaymentTermsCode());
            poDocument.setVendorShippingPaymentTermsCode(poDocument.getVendorContract().getVendorShippingPaymentTermsCode());
            poDocument.setVendorShippingTitleCode(poDocument.getVendorContract().getVendorShippingTitleCode());
        } else {
            VendorDetail vendor = vendorService.getVendorDetail(poDocument.getVendorHeaderGeneratedIdentifier(), poDocument.getVendorDetailAssignedIdentifier());
            if (ObjectUtils.isNotNull(vendor)) {
                poDocument.setVendorPaymentTermsCode(vendor.getVendorPaymentTermsCode());
                poDocument.setVendorShippingPaymentTermsCode(vendor.getVendorShippingPaymentTermsCode());
                poDocument.setVendorShippingTitleCode(vendor.getVendorShippingTitleCode());
            }
        }

        if (!PurapConstants.RequisitionSources.B2B.equals(poDocument.getRequisitionSourceCode())) {
            purapService.addBelowLineItems(poDocument);
        }
        poDocument.fixItemReferences();

        return poDocument;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#getInternalPurchasingDollarLimit(org.kuali.kfs.module.purap.document.PurchaseOrderDocument)
     */
    @Override
    public KualiDecimal getInternalPurchasingDollarLimit(PurchaseOrderDocument document) {
        if ((document.getVendorContract() != null) && (document.getContractManager() != null)) {
            KualiDecimal contractDollarLimit = vendorService.getApoLimitFromContract(document.getVendorContract().getVendorContractGeneratedIdentifier(), document.getChartOfAccountsCode(), document.getOrganizationCode());
            // FIXME somehow data fields such as contractManagerDelegationDollarLimit in reference object contractManager didn't get
            // retrieved
            // (are null) as supposed to be (this happens whether or not proxy is set to true), even though contractManager is not
            // null;
            // so here we have to manually refresh the contractManager to retrieve the fields
            if (document.getContractManager().getContractManagerDelegationDollarLimit() == null) {
                document.refreshReferenceObject(PurapPropertyConstants.CONTRACT_MANAGER);
            }
            KualiDecimal contractManagerLimit = document.getContractManager().getContractManagerDelegationDollarLimit();
            if ((contractDollarLimit != null) && (contractManagerLimit != null)) {
                if (contractDollarLimit.compareTo(contractManagerLimit) > 0) {
                    return contractDollarLimit;
                } else {
                    return contractManagerLimit;
                }
            } else if (contractDollarLimit != null) {
                return contractDollarLimit;
            } else {
                return contractManagerLimit;
            }
        } else if ((document.getVendorContract() == null) && (document.getContractManager() != null)) {
            // FIXME As above, here we have to manually refresh the contractManager to retrieve its field
            if (document.getContractManager().getContractManagerDelegationDollarLimit() == null) {
                document.refreshReferenceObject(PurapPropertyConstants.CONTRACT_MANAGER);
            }
            return document.getContractManager().getContractManagerDelegationDollarLimit();
        } else if ((document.getVendorContract() != null) && (document.getContractManager() == null)) {
            return purapService.getApoLimit(document.getVendorContract().getVendorContractGeneratedIdentifier(), document.getChartOfAccountsCode(), document.getOrganizationCode());
        } else {
            String errorMsg = "No internal purchase order dollar limit found for purchase order '" + document.getPurapDocumentIdentifier() + "'.";
            LOG.warn(errorMsg);
            return null;
        }
    }

    /**
     * Loops through the collection of error messages and adding each of them to the error map.
     *
     * @param errorKey The resource key used to retrieve the error text from the error message resource bundle.
     * @param errors   The collection of error messages.
     */
    protected void addStringErrorMessagesToMessageMap(String errorKey, Collection<String> errors) {
        if (ObjectUtils.isNotNull(errors)) {
            for (String error : errors) {
                LOG.error("Adding error message using error key '" + errorKey + "' with text '" + error + "'");
                GlobalVariables.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, errorKey, error);
            }
        }
    }


    /**
     * TODO RELEASE 3 - QUOTE
     *
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#printPurchaseOrderQuoteRequestsListPDF(org.kuali.kfs.module.purap.document.PurchaseOrderDocument,
     * java.io.ByteArrayOutputStream)
     */
    @Override
    public boolean printPurchaseOrderQuoteRequestsListPDF(String documentNumber, ByteArrayOutputStream baosPDF) {
        PurchaseOrderDocument po = getPurchaseOrderByDocumentNumber(documentNumber);
        String environment = kualiConfigurationService.getPropertyValueAsString(KFSConstants.ENVIRONMENT_KEY);
        Collection<String> generatePDFErrors = printService.generatePurchaseOrderQuoteRequestsListPdf(po, baosPDF);

        if (generatePDFErrors.size() > 0) {
            addStringErrorMessagesToMessageMap(PurapKeyConstants.ERROR_PURCHASE_ORDER_PDF, generatePDFErrors);
            return false;
        } else {
            return true;
        }
    }

    /**
     * TODO RELEASE 3 - QUOTE
     *
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#printPurchaseOrderQuotePDF(org.kuali.kfs.module.purap.document.PurchaseOrderDocument,
     * org.kuali.kfs.module.purap.businessobject.PurchaseOrderVendorQuote, java.io.ByteArrayOutputStream)
     */
    @Override
    public boolean printPurchaseOrderQuotePDF(PurchaseOrderDocument po, PurchaseOrderVendorQuote povq, ByteArrayOutputStream baosPDF) {
        String environment = kualiConfigurationService.getPropertyValueAsString(KFSConstants.ENVIRONMENT_KEY);
        Collection<String> generatePDFErrors = printService.generatePurchaseOrderQuotePdf(po, povq, baosPDF, environment);

        if (generatePDFErrors.size() > 0) {
            addStringErrorMessagesToMessageMap(PurapKeyConstants.ERROR_PURCHASE_ORDER_PDF, generatePDFErrors);
            return false;
        } else {
            return true;
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#performPurchaseOrderFirstTransmitViaPrinting(java.lang.String,
     * java.io.ByteArrayOutputStream)
     */
    @Override
    public void performPurchaseOrderFirstTransmitViaPrinting(PurchaseOrderDocument po) {
        if (ObjectUtils.isNotNull(po.getPurchaseOrderFirstTransmissionTimestamp())) {
            // should not call this method for first transmission if document has already been transmitted
            String errorMsg = "Method to perform first transmit was called on document (doc id " + po.getDocumentNumber() + ") with already filled in 'first transmit date'";
            LOG.error(errorMsg);
            throw new RuntimeException(errorMsg);
        }
        Timestamp currentDate = dateTimeService.getCurrentTimestamp();
        po.setPurchaseOrderFirstTransmissionTimestamp(currentDate);
        po.setPurchaseOrderLastTransmitTimestamp(currentDate);
        po.setOverrideWorkflowButtons(Boolean.FALSE);
        boolean performedAction = purapWorkflowIntegrationService.takeAllActionsForGivenCriteria(po, "Action taken automatically as part of document initial print transmission", PurapConstants.PurchaseOrderStatuses.NODE_DOCUMENT_TRANSMISSION, GlobalVariables.getUserSession().getPerson(), null);
        if (!performedAction) {
            Person systemUserPerson = getPersonService().getPersonByPrincipalName(KFSConstants.SYSTEM_USER);
            purapWorkflowIntegrationService.takeAllActionsForGivenCriteria(po, "Action taken automatically as part of document initial print transmission by user " + GlobalVariables.getUserSession().getPerson().getName(), PurapConstants.PurchaseOrderStatuses.NODE_DOCUMENT_TRANSMISSION, systemUserPerson, KFSConstants.SYSTEM_USER);
        }
        po.setOverrideWorkflowButtons(Boolean.TRUE);
        if (!po.getApplicationDocumentStatus().equals(PurapConstants.PurchaseOrderStatuses.APPDOC_OPEN)) {
            attemptSetupOfInitialOpenOfDocument(po);
            if (shouldAdhocFyi(po.getRequisitionSourceCode())) {
                sendAdhocFyi(po);
            }
        }
        purapService.saveDocumentNoValidation(po);
    }

    /**
     * This method retrieves the parameter which holds the list of Requisition source codes which does not need FYI Notifications .
     *
     * @return
     */
    private boolean shouldAdhocFyi(String reqSourceCode) {
        Collection<String> excludeList = new ArrayList<String>();
        if (parameterService.parameterExists(PurchaseOrderDocument.class, PurapParameterConstants.PO_NOTIFY_EXCLUSIONS)) {
            excludeList = parameterService.getParameterValuesAsString(PurchaseOrderDocument.class, PurapParameterConstants.PO_NOTIFY_EXCLUSIONS);
        }
        return !excludeList.contains(reqSourceCode);
    }


    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#performPurchaseOrderPreviewPrinting(java.lang.String,
     * java.io.ByteArrayOutputStream)
     */
    @Override
    public void performPurchaseOrderPreviewPrinting(String documentNumber, ByteArrayOutputStream baosPDF) {
        performPrintPurchaseOrderPDFOnly(documentNumber, baosPDF);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#performPrintPurchaseOrderPDFOnly(java.lang.String,
     * java.io.ByteArrayOutputStream)
     */
    @Override
    public void performPrintPurchaseOrderPDFOnly(String documentNumber, ByteArrayOutputStream baosPDF) {
        PurchaseOrderDocument po = getPurchaseOrderByDocumentNumber(documentNumber);
        String environment = kualiConfigurationService.getPropertyValueAsString(KFSConstants.ENVIRONMENT_KEY);
        Collection<String> generatePDFErrors = printService.generatePurchaseOrderPdf(po, baosPDF, environment, null);
        if (!generatePDFErrors.isEmpty()) {
            addStringErrorMessagesToMessageMap(PurapKeyConstants.ERROR_PURCHASE_ORDER_PDF, generatePDFErrors);
            throw new ValidationException("printing purchase order for first transmission failed");
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#retransmitPurchaseOrderPDF(org.kuali.kfs.module.purap.document.PurchaseOrderDocument,
     * java.io.ByteArrayOutputStream)
     */
    @Override
    public void retransmitPurchaseOrderPDF(PurchaseOrderDocument po, ByteArrayOutputStream baosPDF) {

        String environment = kualiConfigurationService.getPropertyValueAsString(KFSConstants.ENVIRONMENT_KEY);
        List<PurchaseOrderItem> items = po.getItems();
        List<PurchaseOrderItem> retransmitItems = new ArrayList<PurchaseOrderItem>();
        for (PurchaseOrderItem item : items) {
            if (item.isItemSelectedForRetransmitIndicator()) {
                retransmitItems.add(item);
            }
        }
        Collection<String> generatePDFErrors = printService.generatePurchaseOrderPdfForRetransmission(po, baosPDF, environment, retransmitItems);

        if (generatePDFErrors.size() > 0) {
            addStringErrorMessagesToMessageMap(PurapKeyConstants.ERROR_PURCHASE_ORDER_PDF, generatePDFErrors);
            throw new ValidationException("found errors while trying to print po with doc id " + po.getDocumentNumber());
        }
        po.setPurchaseOrderLastTransmitTimestamp(dateTimeService.getCurrentTimestamp());
        purapService.saveDocumentNoValidation(po);
    }

    /**
     * This method creates a new Purchase Order Document using the given document type based off the given source document. This
     * method will return null if the source document given is null.<br>
     * <br>
     * ** THIS METHOD DOES NOT SAVE EITHER THE GIVEN SOURCE DOCUMENT OR THE NEW DOCUMENT CREATED
     *
     * @param sourceDocument - document the new Purchase Order Document should be based off of in terms of data
     * @param docType        - document type of the potential new Purchase Order Document
     * @return the new Purchase Order Document of the given document type or null if the given source document is null
     * @throws WorkflowException if a new document cannot be created using the given type
     */
    protected PurchaseOrderDocument createPurchaseOrderDocumentFromSourceDocument(PurchaseOrderDocument sourceDocument, String docType) throws WorkflowException {
        if (ObjectUtils.isNull(sourceDocument)) {
            String errorMsg = "Attempting to create new PO of type '" + docType + "' from source PO doc that is null";
            LOG.error(errorMsg);
            throw new RuntimeException(errorMsg);
        }

        PurchaseOrderDocument newPurchaseOrderChangeDocument = (PurchaseOrderDocument) documentService.getNewDocument(docType);
        newPurchaseOrderChangeDocument.setAccountDistributionMethod(sourceDocument.getAccountDistributionMethod());

        Set classesToExclude = new HashSet();
        Class sourceObjectClass = FinancialSystemTransactionalDocumentBase.class;
        classesToExclude.add(sourceObjectClass);
        while (sourceObjectClass.getSuperclass() != null) {
            sourceObjectClass = sourceObjectClass.getSuperclass();
            classesToExclude.add(sourceObjectClass);
        }
        PurApObjectUtils.populateFromBaseWithSuper(sourceDocument, newPurchaseOrderChangeDocument, PurapConstants.uncopyableFieldsForPurchaseOrder(), classesToExclude);
        newPurchaseOrderChangeDocument.getDocumentHeader().setDocumentDescription(sourceDocument.getDocumentHeader().getDocumentDescription());
        newPurchaseOrderChangeDocument.getDocumentHeader().setOrganizationDocumentNumber(sourceDocument.getDocumentHeader().getOrganizationDocumentNumber());
        newPurchaseOrderChangeDocument.getDocumentHeader().setExplanation(sourceDocument.getDocumentHeader().getExplanation());
        newPurchaseOrderChangeDocument.setPurchaseOrderCurrentIndicator(false);
        newPurchaseOrderChangeDocument.setPendingActionIndicator(false);

        // TODO f2f: what is this doing?
        // Need to find a way to make the ManageableArrayList to expand and populating the items and
        // accounts, otherwise it will complain about the account on item 1 is missing.
        for (PurApItem item : (List<PurApItem>) newPurchaseOrderChangeDocument.getItems()) {
            item.getSourceAccountingLines().iterator();
            // we only need to do this once to apply to all items, so we can break out of the loop now
            SequenceAccessorService sas = SpringContext.getBean(SequenceAccessorService.class);
            Integer itemIdentifier = sas.getNextAvailableSequenceNumber("PO_ITM_ID", PurApItem.class).intValue();
            item.setItemIdentifier(itemIdentifier);
        }

        updateCapitalAssetRelatedCollections(newPurchaseOrderChangeDocument);
        newPurchaseOrderChangeDocument.refreshNonUpdateableReferences();

        return newPurchaseOrderChangeDocument;
    }

    protected void updateCapitalAssetRelatedCollections(PurchaseOrderDocument newDocument) {

        for (PurchasingCapitalAssetItem capitalAssetItem : newDocument.getPurchasingCapitalAssetItems()) {
            Integer lineNumber = capitalAssetItem.getPurchasingItem().getItemLineNumber();
            PurApItem newItem = newDocument.getItemByLineNumber(lineNumber.intValue());
            capitalAssetItem.setItemIdentifier(newItem.getItemIdentifier());
            capitalAssetItem.setPurchasingDocument(newDocument);
            capitalAssetItem.setCapitalAssetSystemIdentifier(null);
            CapitalAssetSystem oldSystem = capitalAssetItem.getPurchasingCapitalAssetSystem();
            capitalAssetItem.setPurchasingCapitalAssetSystem(new PurchaseOrderCapitalAssetSystem(oldSystem));

        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#createAndSavePotentialChangeDocument(java.lang.String,
     * java.lang.String, java.lang.String)
     */
    @Override
    public PurchaseOrderDocument createAndSavePotentialChangeDocument(String documentNumber, String docType, String currentDocumentStatusCode) {
        PurchaseOrderDocument currentDocument = getPurchaseOrderByDocumentNumber(documentNumber);

        try {
            PurchaseOrderDocument newDocument = createPurchaseOrderDocumentFromSourceDocument(currentDocument, docType);

            if (ObjectUtils.isNotNull(newDocument)) {
                newDocument.updateAndSaveAppDocStatus(PurapConstants.PurchaseOrderStatuses.APPDOC_CHANGE_IN_PROCESS);

                // set status if needed
                if (StringUtils.isNotBlank(currentDocumentStatusCode)) {
                    currentDocument.updateAndSaveAppDocStatus(currentDocumentStatusCode);
                }
                try {
                    documentService.saveDocument(newDocument, DocumentSystemSaveEvent.class);
                }
                // if we catch a ValidationException it means the new PO doc found errors
                catch (ValidationException ve) {
                    throw ve;
                }
                // if no validation exception was thrown then rules have passed and we are ok to edit the current PO
                currentDocument.setPendingActionIndicator(true);
                savePurchaseOrderData(currentDocument);

                return newDocument;
            } else {
                String errorMsg = "Attempting to create new PO of type '" + docType + "' from source PO doc id " + documentNumber + " returned null for new document";
                LOG.error(errorMsg);
                throw new RuntimeException(errorMsg);
            }
        } catch (WorkflowException we) {
            String errorMsg = "Workflow Exception caught trying to create and save PO document of type '" + docType + "' using source document with doc id '" + documentNumber + "'";
            LOG.error(errorMsg, we);
            throw new RuntimeException(errorMsg, we);
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#createAndRoutePotentialChangeDocument(java.lang.String,
     * java.lang.String, java.lang.String, java.util.List, java.lang.String)
     */
    @Override
    public PurchaseOrderDocument createAndRoutePotentialChangeDocument(String documentNumber, String docType, String annotation, List adhocRoutingRecipients, String currentDocumentStatusCode) {
        PurchaseOrderDocument currentDocument = getPurchaseOrderByDocumentNumber(documentNumber);

        try {
            currentDocument.updateAndSaveAppDocStatus(currentDocumentStatusCode);
        } catch (WorkflowException e) {
            throw new RuntimeException("Error saving routing data while saving document with id " + currentDocument.getDocumentNumber(), e);
        }

        try {
            PurchaseOrderDocument newDocument = createPurchaseOrderDocumentFromSourceDocument(currentDocument, docType);
            newDocument.updateAndSaveAppDocStatus(PurapConstants.PurchaseOrderStatuses.APPDOC_CHANGE_IN_PROCESS);

            if (ObjectUtils.isNotNull(newDocument)) {
                try {
                    // set the pending indictor before routing, so that when routing is done in synch mode, the pending indicator
                    // won't be set again after route finishes and cause inconsistency
                    currentDocument.setPendingActionIndicator(true);
                    documentService.routeDocument(newDocument, annotation, adhocRoutingRecipients);
                }
                // if we catch a ValidationException it means the new PO doc found errors
                catch (ValidationException ve) {
                    // clear the pending indictor if an exception occurs, to leave the existing PO intact
                    currentDocument.setPendingActionIndicator(false);
                    savePurchaseOrderData(currentDocument);

                    throw ve;
                }
                return newDocument;
            } else {
                String errorMsg = "Attempting to create new PO of type '" + docType + "' from source PO doc id " + documentNumber + " returned null for new document";
                LOG.error(errorMsg);
                throw new RuntimeException(errorMsg);
            }
        } catch (WorkflowException we) {
            String errorMsg = "Workflow Exception caught trying to create and route PO document of type '" + docType + "' using source document with doc id '" + documentNumber + "'";
            LOG.error(errorMsg, we);
            throw new RuntimeException(errorMsg, we);
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#createAndSavePurchaseOrderSplitDocument(java.util.List,
     * java.lang.String, boolean)
     */
    @Override
    public PurchaseOrderSplitDocument createAndSavePurchaseOrderSplitDocument(List<PurchaseOrderItem> newPOItems, PurchaseOrderDocument currentDocument, boolean copyNotes, String splitNoteText) {

        if (ObjectUtils.isNull(currentDocument)) {
            String errorMsg = "Attempting to create new PO of type PurchaseOrderSplitDocument from source PO doc that is null";
            LOG.error(errorMsg);
            throw new RuntimeException(errorMsg);
        }
        String documentNumber = currentDocument.getDocumentNumber();

        try {
            // Create the new Split PO document (throws WorkflowException)
            // Assign PO's initiator to Split PO.
            Person person = SpringContext.getBean(PersonService.class).getPerson(currentDocument.getDocumentHeader().getWorkflowDocument().getInitiatorPrincipalId());
            PurchaseOrderSplitDocument newDocument = (PurchaseOrderSplitDocument) documentService.getNewDocument(PurchaseOrderDocTypes.PURCHASE_ORDER_SPLIT_DOCUMENT, person.getPrincipalName());

            if (ObjectUtils.isNotNull(newDocument)) {

                // Prepare for copying fields over from the current document.
                Set<Class> classesToExclude = getClassesToExcludeFromCopy();
                Map<String, Class> uncopyableFields = PurapConstants.UNCOPYABLE_FIELDS_FOR_PO;
                uncopyableFields.putAll(PurapConstants.uncopyableFieldsForSplitPurchaseOrder());

                // Copy all fields over from the current document except the items and the above-specified fields.
                PurApObjectUtils.populateFromBaseWithSuper(currentDocument, newDocument, uncopyableFields, classesToExclude);
                newDocument.getDocumentHeader().setDocumentDescription(currentDocument.getDocumentHeader().getDocumentDescription());
                newDocument.getDocumentHeader().setOrganizationDocumentNumber(currentDocument.getDocumentHeader().getOrganizationDocumentNumber());
                newDocument.setPurchaseOrderCurrentIndicator(true);
                newDocument.setPendingActionIndicator(false);
                newDocument.setAccountDistributionMethod(currentDocument.getAccountDistributionMethod());
                // Add in and renumber the items that the new document should have.
                newDocument.setItems(newPOItems);
                purapService.addBelowLineItems(newDocument);
                newDocument.renumberItems(0);

                newDocument.setPostingYear(currentDocument.getPostingYear());

                if (copyNotes) {
                    // Copy the old notes, except for the one that contains the split note text.
                    List<Note> notes = currentDocument.getNotes();
                    int noteLength = notes.size();
                    if (noteLength > 0) {
                        notes.subList(noteLength - 1, noteLength).clear();
                        for (Note note : notes) {
                            try {
                                Note copyingNote = documentService.createNoteFromDocument(newDocument, note.getNoteText());
                                newDocument.addNote(copyingNote);
                                noteService.saveNoteList(notes);
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
                newDocument.updateAndSaveAppDocStatus(PurapConstants.PurchaseOrderStatuses.APPDOC_IN_PROCESS);

                // fix references before saving
                fixItemReferences(newDocument);
                newDocument.clearCapitalAssetFields();
                // need to save the document first before creating the note
                purapService.saveDocumentNoValidation(newDocument);

                // Modify the split note text and add the note.
                splitNoteText = splitNoteText.substring(splitNoteText.indexOf(":") + 1);
                splitNoteText = PurapConstants.PODocumentsStrings.SPLIT_NOTE_PREFIX_NEW_DOC + currentDocument.getPurapDocumentIdentifier() + " : " + splitNoteText;
                try {
                    Note splitNote = documentService.createNoteFromDocument(newDocument, splitNoteText);
                    newDocument.addNote(splitNote);
                    noteService.save(splitNote);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }

                return newDocument;
            } else {
                String errorMsg = "Attempting to create new PO of type 'PurchaseOrderSplitDocument' from source PO doc id " + documentNumber + " returned null for new document";
                LOG.error(errorMsg);
                throw new RuntimeException(errorMsg);
            }
        } catch (WorkflowException we) {
            String errorMsg = "Workflow Exception caught trying to create and save PO document of type PurchaseOrderSplitDocument using source document with doc id '" + documentNumber + "'";
            LOG.error(errorMsg, we);
            throw new RuntimeException(errorMsg, we);
        }
    }

    /**
     * Gets a set of classes to exclude from those whose fields will be copied during a copy operation from one Document to another.
     *
     * @return A Set<Class>
     */
    protected Set<Class> getClassesToExcludeFromCopy() {
        Set<Class> classesToExclude = new HashSet<Class>();
        Class sourceObjectClass = DocumentBase.class;
        classesToExclude.add(sourceObjectClass);
        while (sourceObjectClass.getSuperclass() != null) {
            sourceObjectClass = sourceObjectClass.getSuperclass();
            classesToExclude.add(sourceObjectClass);
        }
        return classesToExclude;
    }

    /**
     * Returns the current route node name.
     *
     * @param wd The KualiWorkflowDocument object whose current route node we're trying to get.
     * @return The current route node name.
     * @throws WorkflowException
     */
    protected String getCurrentRouteNodeName(WorkflowDocument wd) throws WorkflowException {
        String[] nodeNames = (String[]) wd.getNodeNames().toArray();
        if ((nodeNames == null) || (nodeNames.length == 0)) {
            return null;
        } else {
            return nodeNames[0];
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#completePurchaseOrder(org.kuali.kfs.module.purap.document.PurchaseOrderDocument)
     */
    @Override
    public void completePurchaseOrder(PurchaseOrderDocument po) {
        LOG.debug("completePurchaseOrder() started");
        setCurrentAndPendingIndicatorsForApprovedPODocuments(po);
        setupDocumentForPendingFirstTransmission(po);

        // check thresholds to see if receiving is required for purchase order
        if (!po.isReceivingDocumentRequiredIndicator()) {
            setReceivingRequiredIndicatorForPurchaseOrder(po);
        }

        // update the vendor record if the commodity code used on the PO is not already associated with the vendor.
        updateVendorCommodityCode(po);

        // PERFORM ANY LOGIC THAT COULD POTENTIALLY CAUSE THE DOCUMENT TO FAIL BEFORE THIS LINE
        // FOLLOWING LINES COULD INVOLVE TRANSMITTING THE PO TO THE VENDOR WHICH WILL NOT BE REVERSED IN A TRANSACTION ROLLBACK

        // if the document is set in a Pending Transmission status then don't OPEN the PO just leave it as is
        if (!PurchaseOrderStatuses.STATUSES_BY_TRANSMISSION_TYPE.values().contains(po.getApplicationDocumentStatus())) {
            attemptSetupOfInitialOpenOfDocument(po);
        } else if (PurchaseOrderStatuses.APPDOC_PENDING_CXML.equals(po.getApplicationDocumentStatus())) {
            completeB2BPurchaseOrder(po);
        } else if (PurchaseOrderStatuses.APPDOC_PENDING_PRINT.equals(po.getApplicationDocumentStatus())) {
            // default to using user that routed PO
            String userToRouteFyi = po.getDocumentHeader().getWorkflowDocument().getRoutedByPrincipalId();
            if (po.getPurchaseOrderAutomaticIndicator()) {
                // if APO, use the user that initiated the requisition
                RequisitionDocument req = requisitionService.getRequisitionById(po.getRequisitionIdentifier());
                userToRouteFyi = req.getDocumentHeader().getWorkflowDocument().getInitiatorPrincipalId();
            }

            Set<String> currentNodes = po.getDocumentHeader().getWorkflowDocument().getCurrentNodeNames();
            if (CollectionUtils.isNotEmpty(currentNodes)) {
                po.getDocumentHeader().getWorkflowDocument().adHocToPrincipal(ActionRequestType.FYI, currentNodes.iterator().next(), "This PO is ready for printing and distribution.", userToRouteFyi, "", true, "PRINT");

            }
        }

    }

    protected boolean completeB2BPurchaseOrder(PurchaseOrderDocument po) {
        String errors = b2bPurchaseOrderService.sendPurchaseOrder(po);
        if (StringUtils.isEmpty(errors)) {
            // PO sent successfully; change status to OPEN
            attemptSetupOfInitialOpenOfDocument(po);
            po.setPurchaseOrderLastTransmitTimestamp(dateTimeService.getCurrentTimestamp());
            return true;
        } else {
            // PO transmission failed; record errors and change status to "cxml failed"
            try {
                String noteText = "Unable to transmit the PO for the following reasons:\n" + errors;
                int noteMaxSize = dataDictionaryService.getAttributeMaxLength("Note", "noteText");

                // Break up the note into multiple pieces if the note is too large to fit in the database field.
                while (noteText.length() > noteMaxSize) {
                    int fromIndex = 0;
                    String noteText1 = noteText.substring(0, noteMaxSize);
                    Note note1 = documentService.createNoteFromDocument(po, noteText1);
                    po.addNote(note1);
                    noteText = noteText.substring(noteMaxSize);
                }

                Note note = documentService.createNoteFromDocument(po, noteText);
                po.addNote(note);
                documentService.saveDocumentNotes(po);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            try {
                po.updateAndSaveAppDocStatus(PurchaseOrderStatuses.APPDOC_CXML_ERROR);
            } catch (WorkflowException e) {
                throw new RuntimeException("Error saving routing data while saving document with id " + po.getDocumentNumber(), e);
            }

            return false;
        }
    }

    @Override
    public void retransmitB2BPurchaseOrder(PurchaseOrderDocument po) {
        if (completeB2BPurchaseOrder(po)) {
            KNSGlobalVariables.getMessageList().add(PurapKeyConstants.B2B_PO_RETRANSMIT_SUCCESS);
        } else {
            GlobalVariables.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, PurapKeyConstants.B2B_PO_RETRANSMIT_FAILED);
        }
        purapService.saveDocumentNoValidation(po);
    }

    @Override
    public void completePurchaseOrderAmendment(PurchaseOrderDocument poa) {
        LOG.debug("completePurchaseOrderAmendment() started");

        setCurrentAndPendingIndicatorsForApprovedPODocuments(poa);

        // check thresholds to see if receiving is required for purchase order amendment
        if (!poa.isReceivingDocumentRequiredIndicator() && !SpringContext.getBean(PaymentRequestService.class).hasActivePaymentRequestsForPurchaseOrder(poa.getPurapDocumentIdentifier())) {
            setReceivingRequiredIndicatorForPurchaseOrder(poa);
        }

        // if unordered items have been added to the PO then send an FYI to all fiscal officers
        if (hasNewUnorderedItem(poa)) {
            sendFyiForNewUnorderedItems(poa);
        }

    }

    /**
     * First we check that vendor commodity codes should indeed be added, and if so-
     * If there are commodity codes on the items on the PurchaseOrderDocument that haven't existed yet on the vendor that the
     * PurchaseOrderDocument is using, then we will spawn a new VendorDetailMaintenanceDocument automatically to update the vendor
     * with the commodity codes that aren't already existing on the vendor.
     *
     * @param po The PurchaseOrderDocument containing the vendor that we want to update.
     */
    @Override
    public void updateVendorCommodityCode(PurchaseOrderDocument po) {
        String noteText = "";
        VendorDetail oldVendorDetail = po.getVendorDetail();
        VendorDetail newVendorDetail = updateVendorWithMissingCommodityCodesIfNecessary(po);

        //we default to adding vendor commodity codes.
        Boolean shouldUpdate = parameterService.getParameterValueAsBoolean(RequisitionDocument.class, PurapParameterConstants.UPDATE_VENDOR_SETTING, Boolean.TRUE);
        if (shouldUpdate && newVendorDetail != null) {
            try {
                // spawn a new vendor maintenance document to add the note
                MaintenanceDocument vendorMaintDoc = null;
                try {
                    vendorMaintDoc = (MaintenanceDocument) documentService.getNewDocument("PVEN");
                    vendorMaintDoc.getDocumentHeader().setDocumentDescription("Automatically spawned from PO");
                    vendorMaintDoc.getOldMaintainableObject().setBusinessObject(oldVendorDetail);
                    vendorMaintDoc.getNewMaintainableObject().setBusinessObject(newVendorDetail);
                    vendorMaintDoc.getNewMaintainableObject().setMaintenanceAction(KFSConstants.MAINTENANCE_EDIT_ACTION);
                    vendorMaintDoc.getNewMaintainableObject().setDocumentNumber(vendorMaintDoc.getDocumentNumber());
                    boolean isVendorLocked = checkForLockingDocument(vendorMaintDoc);
                    if (!isVendorLocked) {
                        // validating vendor doc to capture exception before trying to route which if exception happens in
                        // docService, then PO will fail too
                        vendorMaintDoc.validateBusinessRules(new RouteDocumentEvent(vendorMaintDoc));
                        addNoteForCommodityCodeToVendor(vendorMaintDoc.getNewMaintainableObject(), vendorMaintDoc.getDocumentNumber(), po.getPurapDocumentIdentifier());
                        documentService.routeDocument(vendorMaintDoc, null, null);
                    } else {
                        // Add a note to the PO to tell the users that we can't automatically update the vendor because it's locked.
                        noteText = "Unable to automatically update vendor because it is locked";
                    }
                } catch (Exception e) {
                    if (ObjectUtils.isNull(vendorMaintDoc)) {
                        noteText = "Unable to create a new VendorDetailMaintenanceDocument to update the vendor with new commodity codes";
                    } else {
                        noteText = "Unable to route a new VendorDetailMaintenanceDocument to update the vendor with new commodity codes";
                    }
                } finally {
                    if (StringUtils.isNotBlank(noteText)) {
                        // update on purchase order notes
                        Note note = documentService.createNoteFromDocument(po, noteText);
                        po.addNote(note);
                        noteService.save(note);
                        if (GlobalVariables.getMessageMap().hasErrors()) {
                            // clear out GlobalVariable message map, since we have taken care of the errors
                            //If errors were discovered during the routing of the Vendor, although the exception is caught, the errors are still added to the message map.
                            //This is causing the PO to go into exception routing although the error was only with the Vendor doc
                            GlobalVariables.setMessageMap(new MessageMap());
                        }
                    }
                }
            } catch (Exception e) {
                LOG.error("updateVendorCommodityCode() unable to add a note(" + noteText + ") to PO document " + po.getDocumentNumber());
            }
        }
    }

    /**
     * Creates a note to be added to the Vendor Maintenance Document which is spawned from the PurchaseOrderDocument.
     *
     * @param maintainable
     * @param documentNumber
     * @param poID
     */
    protected void addNoteForCommodityCodeToVendor(Maintainable maintainable, String documentNumber, Integer poID) {
        Note newBONote = new Note();
        newBONote.setNoteText("Change vendor document ID <" + documentNumber + ">. Document was automatically created from PO <" + poID + "> to add commodity codes used on this PO that were not yet assigned to this vendor.");
        try {

            newBONote = noteService.createNote(newBONote, maintainable.getBusinessObject(), GlobalVariables.getUserSession().getPrincipalId());
            newBONote.setNotePostedTimestampToCurrent();
        } catch (Exception e) {
            throw new RuntimeException("Caught Exception While Trying To Add Note to Vendor", e);
        }
        List<Note> noteList = noteService.getByRemoteObjectId(maintainable.getBusinessObject().getObjectId());
        noteList.add(newBONote);
        noteService.saveNoteList(noteList);
    }

    /**
     * Checks whether the vendor is currently locked.
     *
     * @param document The MaintenanceDocument containing the vendor.
     * @return boolean true if the vendor is currently locked and false otherwise.
     */
    protected boolean checkForLockingDocument(MaintenanceDocument document) {
        String blockingDocId = maintenanceDocumentService.getLockingDocumentId(document);
        if (StringUtils.isBlank(blockingDocId)) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#updateVendorWithMissingCommodityCodesIfNecessary(org.kuali.kfs.module.purap.document.PurchaseOrderDocument)
     */
    @Override
    public VendorDetail updateVendorWithMissingCommodityCodesIfNecessary(PurchaseOrderDocument po) {
        List<CommodityCode> result = new ArrayList<CommodityCode>();
        boolean foundDefault = false;
        VendorDetail vendor = (VendorDetail) ObjectUtils.deepCopy(po.getVendorDetail());
        for (PurchaseOrderItem item : (List<PurchaseOrderItem>) po.getItems()) {
            // Only check on commodity codes if the item is active and is above the line item type.
            if (item.getItemType().isLineItemIndicator() && item.isItemActiveIndicator()) {
                CommodityCode cc = item.getCommodityCode();
                if (cc != null && !result.contains(cc)) {
                    List<VendorCommodityCode> vendorCommodityCodes = po.getVendorDetail().getVendorCommodities();
                    boolean foundMatching = false;
                    for (VendorCommodityCode vcc : vendorCommodityCodes) {
                        if (vcc.getCommodityCode().getPurchasingCommodityCode().equals(cc.getPurchasingCommodityCode())) {
                            foundMatching = true;
                        }
                        if (!foundDefault && vcc.isCommodityDefaultIndicator()) {
                            foundDefault = true;
                        }
                    }
                    if (!foundMatching) {
                        result.add(cc);
                        VendorCommodityCode vcc = new VendorCommodityCode(vendor.getVendorHeaderGeneratedIdentifier(), vendor.getVendorDetailAssignedIdentifier(), cc, true);
                        vcc.setActive(true);
                        if (!foundDefault) {
                            vcc.setCommodityDefaultIndicator(true);
                            foundDefault = true;
                        }
                        vendor.getVendorCommodities().add(vcc);
                    }
                }
            }
        }
        if (result.size() > 0) {
            // We also have to add to the old vendor detail's vendorCommodities if we're adding to the new
            // vendor detail's vendorCommodities.
            for (int i = 0; i < result.size(); i++) {
                po.getVendorDetail().getVendorCommodities().add(new VendorCommodityCode());
            }
            return vendor;
        } else {
            return null;
        }
    }

    /**
     * Update the purchase order document with the appropriate status for pending first transmission based on the transmission type.
     *
     * @param po The purchase order document whose status to be updated.
     */
    protected void setupDocumentForPendingFirstTransmission(PurchaseOrderDocument po) {
        if (POTransmissionMethods.PRINT.equals(po.getPurchaseOrderTransmissionMethodCode()) || POTransmissionMethods.FAX.equals(po.getPurchaseOrderTransmissionMethodCode()) || POTransmissionMethods.ELECTRONIC.equals(po.getPurchaseOrderTransmissionMethodCode())) {
            String newStatusCode = PurchaseOrderStatuses.STATUSES_BY_TRANSMISSION_TYPE.get(po.getPurchaseOrderTransmissionMethodCode());
            if (LOG.isDebugEnabled()) {
                LOG.debug("setupDocumentForPendingFirstTransmission() Purchase Order Transmission Type is '" + po.getPurchaseOrderTransmissionMethodCode() + "' setting status to '" + newStatusCode + "'");
            }
            try {
                po.updateAndSaveAppDocStatus(newStatusCode);
            } catch (WorkflowException e) {
                throw new RuntimeException("Error saving routing data while saving document with id " + po.getDocumentNumber(), e);
            }
        }
    }

    /**
     * If the status of the purchase order is not OPEN and the initial open date is null, sets the initial open date to current date
     * and update the status to OPEN, then save the purchase order.
     *
     * @param po The purchase order document whose initial open date and status we want to update.
     */
    protected void attemptSetupOfInitialOpenOfDocument(PurchaseOrderDocument po) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("attemptSetupOfInitialOpenOfDocument() started using document with doc id " + po.getDocumentNumber());
        }

        if (!PurchaseOrderStatuses.APPDOC_OPEN.equals(po.getApplicationDocumentStatus())) {
            if (ObjectUtils.isNull(po.getPurchaseOrderInitialOpenTimestamp())) {
                LOG.debug("attemptSetupOfInitialOpenOfDocument() setting initial open date on document");
                po.setPurchaseOrderInitialOpenTimestamp(dateTimeService.getCurrentTimestamp());
            } else {
                throw new RuntimeException("Document does not have status code '" + PurchaseOrderStatuses.APPDOC_OPEN + "' on it but value of initial open date is " + po.getPurchaseOrderInitialOpenTimestamp());
            }
            LOG.info("attemptSetupOfInitialOpenOfDocument() Setting po document id " + po.getDocumentNumber() + " status from '" + po.getApplicationDocumentStatus() + "' to '" + PurchaseOrderStatuses.APPDOC_OPEN + "'");
            try {
                po.updateAndSaveAppDocStatus(PurchaseOrderStatuses.APPDOC_OPEN);
            } catch (WorkflowException we) {
                throw new RuntimeException("Unable to load a WorkflowDocument object for " + po.getDocumentNumber(), we);
            }
        } else {
            LOG.error("attemptSetupOfInitialOpenOfDocument() Found document already in '" + PurchaseOrderStatuses.APPDOC_OPEN + "' status for PO#" + po.getPurapDocumentIdentifier() + "; will not change or update");
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#getCurrentPurchaseOrder(java.lang.Integer)
     */
    @Override
    public PurchaseOrderDocument getCurrentPurchaseOrder(Integer id) {
        return getPurchaseOrderByDocumentNumber(purchaseOrderDao.getDocumentNumberForCurrentPurchaseOrder(id));
        // TODO hjs: code review (why is this DB call so complicated? wouldn't this method be cleaner and less db calls?)
        // return purchaseOrderDao.getCurrentPurchaseOrder(id);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#getPurchaseOrderByDocumentNumber(java.lang.String)
     */
    @Override
    public PurchaseOrderDocument getPurchaseOrderByDocumentNumber(String documentNumber) {
        if (ObjectUtils.isNotNull(documentNumber)) {
            try {
                PurchaseOrderDocument doc = (PurchaseOrderDocument) documentService.getByDocumentHeaderId(documentNumber);
                if (ObjectUtils.isNotNull(doc)) {
                    WorkflowDocument workflowDocument = doc.getDocumentHeader().getWorkflowDocument();
                    doc.refreshReferenceObject(KFSPropertyConstants.DOCUMENT_HEADER);
                    doc.getDocumentHeader().setWorkflowDocument(workflowDocument);
                }
                return doc;
            } catch (WorkflowException e) {
                String errorMessage = "Error getting purchase order document from document service";
                LOG.error("getPurchaseOrderByDocumentNumber() " + errorMessage, e);
                throw new RuntimeException(errorMessage, e);
            }
        }
        return null;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#getOldestPurchaseOrder(org.kuali.kfs.module.purap.document.PurchaseOrderDocument,
     * org.kuali.kfs.module.purap.document.PurchaseOrderDocument)
     */
    @Override
    public PurchaseOrderDocument getOldestPurchaseOrder(PurchaseOrderDocument po, PurchaseOrderDocument documentBusinessObject) {
        LOG.debug("entering getOldestPO(PurchaseOrderDocument)");
        if (ObjectUtils.isNotNull(po)) {
            String oldestDocumentNumber = purchaseOrderDao.getOldestPurchaseOrderDocumentNumber(po.getPurapDocumentIdentifier());
            // KFSMI-9746 -- See Harsha's comments...
            if (StringUtils.isBlank(oldestDocumentNumber)) {
                return null;
            }
            if (StringUtils.equals(oldestDocumentNumber, po.getDocumentNumber())) {
                // manually set bo notes - this is mainly done for performance reasons (preferably we could call
                // retrieve doc notes in PersistableBusinessObjectBase but that is protected)
                updateNotes(po, documentBusinessObject);
                LOG.debug("exiting getOldestPO(PurchaseOrderDocument)");
                return po;
            } else {
                PurchaseOrderDocument oldestPurchaseOrder = getPurchaseOrderByDocumentNumber(oldestDocumentNumber);
                updateNotes(oldestPurchaseOrder, documentBusinessObject);
                LOG.debug("exiting getOldestPO(PurchaseOrderDocument)");
                return oldestPurchaseOrder;
            }
        }
        return null;
    }

    /**
     * If the purchase order's object id is not null (I think this means if it's an existing purchase order that had already been
     * saved to the db previously), get the notes of the purchase order from the database, fix the notes' fields by calling the
     * fixDbNoteFields, then set the notes to the purchase order. Otherwise (I think this means if it's a new purchase order), set
     * the notes of this purchase order to be the notes of the documentBusinessObject.
     *
     * @param po                     The current purchase order.
     * @param documentBusinessObject The oldest purchase order whose purapDocumentIdentifier is the same as the po's
     *                               purapDocumentIdentifier.
     */
    protected void updateNotes(PurchaseOrderDocument po, PurchaseOrderDocument documentBusinessObject) {
        if (ObjectUtils.isNotNull(documentBusinessObject)) {
            if (ObjectUtils.isNotNull(po.getObjectId())) {
                List<Note> dbNotes = noteService.getByRemoteObjectId(po.getObjectId());
                // need to set fields that are not ojb managed (i.e. the notes on the documentBusinessObject may have been modified
                // independently of the ones in the db)
                fixDbNoteFields(documentBusinessObject, dbNotes);
                po.setNotes(dbNotes);
            } else {
                po.setNotes(documentBusinessObject.getNotes());
            }
        }
    }

    /**
     * This method fixes non ojb managed missing fields from the db
     *
     * @param documentBusinessObject The oldest purchase order whose purapDocumentIdentifier is the same as the po's
     *                               purapDocumentIdentifier.
     * @param dbNotes                The notes of the purchase order obtained from the database.
     */
    protected void fixDbNoteFields(PurchaseOrderDocument documentBusinessObject, List<Note> dbNotes) {
        for (int i = 0; i < dbNotes.size(); i++) {
            Note dbNote = dbNotes.get(i);
            List<Note> currentNotes = documentBusinessObject.getNotes();
            if (i < currentNotes.size()) {
                Note currentNote = (currentNotes).get(i);
                // set the fyi from the current note if not empty
                AdHocRouteRecipient fyiNoteRecipient = currentNote.getAdHocRouteRecipient();
                if (ObjectUtils.isNotNull(fyiNoteRecipient)) {
                    dbNote.setAdHocRouteRecipient(fyiNoteRecipient);
                }
            }
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#getPurchaseOrderNotes(java.lang.Integer)
     */

    @Override
    public List<Note> getPurchaseOrderNotes(Integer id) {
        List<Note> notes = new ArrayList<Note>();
        PurchaseOrderDocument po = getPurchaseOrderByDocumentNumber(purchaseOrderDao.getOldestPurchaseOrderDocumentNumber(id));

        if (ObjectUtils.isNotNull(po)) {

            notes = noteService.getByRemoteObjectId(po.getDocumentHeader().getObjectId());
        }
        return notes;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#setCurrentAndPendingIndicatorsForApprovedPODocuments(org.kuali.kfs.module.purap.document.PurchaseOrderDocument)
     */
    @Override
    public void setCurrentAndPendingIndicatorsForApprovedPODocuments(PurchaseOrderDocument newPO) {
        // Get the "current PO" that's in the database, i.e. the PO row that contains current indicator = Y
        PurchaseOrderDocument oldPO = getCurrentPurchaseOrder(newPO.getPurapDocumentIdentifier());

        // If the document numbers between the oldPO and the newPO are different, then this is a PO change document.
        if (!oldPO.getDocumentNumber().equals(newPO.getDocumentNumber())) {
            // First, we set the indicators for the oldPO to : Current = N and Pending = N
            oldPO.setPurchaseOrderCurrentIndicator(false);
            oldPO.setPendingActionIndicator(false);

            // set the status and status history of the oldPO to retired version
            try {
                oldPO.updateAndSaveAppDocStatus(PurapConstants.PurchaseOrderStatuses.APPDOC_RETIRED_VERSION);
            } catch (WorkflowException e) {
                throw new RuntimeException("Error saving routing data while saving document with id " + oldPO.getDocumentNumber(), e);
            }

            savePurchaseOrderData(oldPO);
        }

        // Now, we set the "new PO" indicators so that Current = Y and Pending = N
        newPO.setPurchaseOrderCurrentIndicator(true);
        newPO.setPendingActionIndicator(false);

        // KFSMI-9879 - Don't save the newPO here - if it's a PO Close Doc, that could delete previously
        // saved GLPEs.
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#setCurrentAndPendingIndicatorsForDisapprovedChangePODocuments(org.kuali.kfs.module.purap.document.PurchaseOrderDocument)
     */
    @Override
    public void setCurrentAndPendingIndicatorsForDisapprovedChangePODocuments(PurchaseOrderDocument newPO) {
        updateCurrentDocumentForNoPendingAction(newPO, PurapConstants.PurchaseOrderStatuses.APPDOC_DISAPPROVED_CHANGE, PurapConstants.PurchaseOrderStatuses.APPDOC_OPEN);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#setCurrentAndPendingIndicatorsForCancelledChangePODocuments(org.kuali.kfs.module.purap.document.PurchaseOrderDocument)
     */
    @Override
    public void setCurrentAndPendingIndicatorsForCancelledChangePODocuments(PurchaseOrderDocument newPO) {
        updateCurrentDocumentForNoPendingAction(newPO, PurapConstants.PurchaseOrderStatuses.APPDOC_CANCELLED_CHANGE, PurapConstants.PurchaseOrderStatuses.APPDOC_OPEN);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#setCurrentAndPendingIndicatorsForCancelledReopenPODocuments(org.kuali.kfs.module.purap.document.PurchaseOrderDocument)
     */
    @Override
    public void setCurrentAndPendingIndicatorsForCancelledReopenPODocuments(PurchaseOrderDocument newPO) {
        updateCurrentDocumentForNoPendingAction(newPO, PurapConstants.PurchaseOrderStatuses.APPDOC_CANCELLED_CHANGE, PurapConstants.PurchaseOrderStatuses.APPDOC_CLOSED);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#setCurrentAndPendingIndicatorsForDisapprovedReopenPODocuments(org.kuali.kfs.module.purap.document.PurchaseOrderDocument)
     */
    @Override
    public void setCurrentAndPendingIndicatorsForDisapprovedReopenPODocuments(PurchaseOrderDocument newPO) {
        updateCurrentDocumentForNoPendingAction(newPO, PurapConstants.PurchaseOrderStatuses.APPDOC_DISAPPROVED_CHANGE, PurapConstants.PurchaseOrderStatuses.APPDOC_CLOSED);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#setCurrentAndPendingIndicatorsForCancelledRemoveHoldPODocuments(org.kuali.kfs.module.purap.document.PurchaseOrderDocument)
     */
    @Override
    public void setCurrentAndPendingIndicatorsForCancelledRemoveHoldPODocuments(PurchaseOrderDocument newPO) {
        updateCurrentDocumentForNoPendingAction(newPO, PurapConstants.PurchaseOrderStatuses.APPDOC_CANCELLED_CHANGE, PurapConstants.PurchaseOrderStatuses.APPDOC_PAYMENT_HOLD);
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#setCurrentAndPendingIndicatorsForDisapprovedRemoveHoldPODocuments(org.kuali.kfs.module.purap.document.PurchaseOrderDocument)
     */
    @Override
    public void setCurrentAndPendingIndicatorsForDisapprovedRemoveHoldPODocuments(PurchaseOrderDocument newPO) {
        updateCurrentDocumentForNoPendingAction(newPO, PurapConstants.PurchaseOrderStatuses.APPDOC_DISAPPROVED_CHANGE, PurapConstants.PurchaseOrderStatuses.APPDOC_PAYMENT_HOLD);
    }

    /**
     * Update the statuses of both the old purchase order and the new purchase orders, then save the old and the new purchase
     * orders.
     *
     * @param newPO       The new change purchase order document (e.g. the PurchaseOrderAmendmentDocument that was resulted from the user
     *                    clicking on the amend button).
     * @param newPOStatus The status to be set on the new change purchase order document.
     * @param oldPOStatus The status to be set on the existing (old) purchase order document.
     */
    protected void updateCurrentDocumentForNoPendingAction(PurchaseOrderDocument newPO, String newPOStatus, String oldPOStatus) {
        // Get the "current PO" that's in the database, i.e. the PO row that contains current indicator = Y
        PurchaseOrderDocument oldPO = getCurrentPurchaseOrder(newPO.getPurapDocumentIdentifier());
        // Set the Pending indicator for the oldPO to N
        oldPO.setPendingActionIndicator(false);
        try {
            oldPO.updateAndSaveAppDocStatus(oldPOStatus);
            newPO.updateAndSaveAppDocStatus(newPOStatus);
        } catch (WorkflowException e) {
            throw new RuntimeException("Error saving routing data while saving document", e);
        }

        savePurchaseOrderData(oldPO);
        saveDocumentNoValidationUsingClearMessageMap(newPO);
    }

    @Override
    public List<PurchaseOrderQuoteStatus> getPurchaseOrderQuoteStatusCodes() {
        List<PurchaseOrderQuoteStatus> poQuoteStatuses = new ArrayList<PurchaseOrderQuoteStatus>();
        poQuoteStatuses = (List<PurchaseOrderQuoteStatus>) businessObjectService.findAll(PurchaseOrderQuoteStatus.class);
        return poQuoteStatuses;
    }

    @Override
    public void setReceivingRequiredIndicatorForPurchaseOrder(PurchaseOrderDocument po) {
        ThresholdHelper thresholdHelper = new ThresholdHelper(po);
        boolean result = thresholdHelper.isReceivingDocumentRequired();
        if (result) {
            ThresholdSummary thresholdSummary = thresholdHelper.getThresholdSummary();
            ReceivingThreshold receivingThreshold = thresholdHelper.getReceivingThreshold();
            po.setReceivingDocumentRequiredIndicator(true);

            String notetxt = "Receiving is set to be required because the threshold summary with a total amount of " + thresholdSummary.getTotalAmount();
            notetxt += " exceeds the receiving threshold of " + receivingThreshold.getThresholdAmount();
            notetxt += " with respect to the threshold criteria ";

            if (thresholdSummary.getThresholdCriteria() == ThresholdHelper.CHART) {
                notetxt += " Chart " + receivingThreshold.getChartOfAccountsCode();
            } else if (thresholdSummary.getThresholdCriteria() == ThresholdHelper.CHART_AND_ACCOUNTTYPE) {
                notetxt += " Chart " + receivingThreshold.getChartOfAccountsCode();
                notetxt += " - Account Type " + receivingThreshold.getAccountTypeCode();
            } else if (thresholdSummary.getThresholdCriteria() == ThresholdHelper.CHART_AND_SUBFUND) {
                notetxt += " Chart " + receivingThreshold.getChartOfAccountsCode();
                notetxt += " - Sub-Fund " + receivingThreshold.getSubFundGroupCode();
            } else if (thresholdSummary.getThresholdCriteria() == ThresholdHelper.CHART_AND_COMMODITYCODE) {
                notetxt += " Chart " + receivingThreshold.getChartOfAccountsCode();
                notetxt += " - Commodity Code " + receivingThreshold.getPurchasingCommodityCode();
            } else if (thresholdSummary.getThresholdCriteria() == ThresholdHelper.CHART_AND_OBJECTCODE) {
                notetxt += " Chart " + receivingThreshold.getChartOfAccountsCode();
                notetxt += " - Object code " + receivingThreshold.getFinancialObjectCode();
            } else if (thresholdSummary.getThresholdCriteria() == ThresholdHelper.CHART_AND_ORGANIZATIONCODE) {
                notetxt += " Chart " + receivingThreshold.getChartOfAccountsCode();
                notetxt += " - Organization " + receivingThreshold.getOrganizationCode();
            } else if (thresholdSummary.getThresholdCriteria() == ThresholdHelper.CHART_AND_VENDOR) {
                notetxt += " Chart " + receivingThreshold.getChartOfAccountsCode();
                notetxt += " - Vendor " + receivingThreshold.getVendorNumber();
            }

            try {
                Note note = documentService.createNoteFromDocument(po, notetxt);
                // documentService.addNoteToDocument(po, note);
                noteService.save(note);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#hasNewUnorderedItem(org.kuali.kfs.module.purap.document.PurchaseOrderDocument)
     */
    @Override
    public boolean hasNewUnorderedItem(PurchaseOrderDocument po) {

        boolean itemAdded = false;

        for (PurchaseOrderItem poItem : (List<PurchaseOrderItem>) po.getItems()) {
            // only check, active, above the line, unordered items
            if (poItem.isItemActiveIndicator() && poItem.getItemType().isLineItemIndicator() && PurapConstants.ItemTypeCodes.ITEM_TYPE_UNORDERED_ITEM_CODE.equals(poItem.getItemTypeCode())) {

                // if the item identifier is null its new, or if the item doesn't exist on the current purchase order it's new
                if (poItem.getItemIdentifier() == null || !purchaseOrderDao.itemExistsOnPurchaseOrder(poItem.getItemLineNumber(), purchaseOrderDao.getDocumentNumberForCurrentPurchaseOrder(po.getPurapDocumentIdentifier()))) {
                    itemAdded = true;
                    break;
                }
            }
        }

        return itemAdded;
    }

    @Override
    public boolean isNewUnorderedItem(PurchaseOrderItem poItem) {

        boolean itemAdded = false;

        // only check, active, above the line, unordered items
        if (poItem.isItemActiveIndicator() && poItem.getItemType().isLineItemIndicator() && PurapConstants.ItemTypeCodes.ITEM_TYPE_UNORDERED_ITEM_CODE.equals(poItem.getItemTypeCode())) {

            // if the item identifier is null its new, or if the item doesn't exist on the current purchase order it's new
            if (poItem.getItemIdentifier() == null || !purchaseOrderDao.itemExistsOnPurchaseOrder(poItem.getItemLineNumber(), purchaseOrderDao.getDocumentNumberForCurrentPurchaseOrder(poItem.getPurchaseOrder().getPurapDocumentIdentifier()))) {
                itemAdded = true;
            }
        }

        return itemAdded;
    }

    @Override
    public boolean isNewItemForAmendment(PurchaseOrderItem poItem) {

        boolean itemAdded = false;

        // only check, active, above the line, unordered items
        if (poItem.isItemActiveIndicator() && poItem.getItemType().isLineItemIndicator()) {

            // if the item identifier is null its new, or if the item doesn't exist on the current purchase order it's new
            Integer docId = poItem.getPurapDocumentIdentifier();
            if (docId == null) {
                docId = poItem.getPurchaseOrder().getPurapDocumentIdentifier();
            }
            if (poItem.getItemIdentifier() == null || !purchaseOrderDao.itemExistsOnPurchaseOrder(poItem.getItemLineNumber(), purchaseOrderDao.getDocumentNumberForCurrentPurchaseOrder(docId))) {
                itemAdded = true;
            }
        }

        return itemAdded;
    }

    /**
     * Sends an FYI to fiscal officers for new unordered items.
     *
     * @param po
     */
    protected void sendFyiForNewUnorderedItems(PurchaseOrderDocument po) {

        List<AdHocRoutePerson> fyiList = createFyiFiscalOfficerListForNewUnorderedItems(po);
        String annotation = "Notification of New Unordered Items for Purchase Order" + po.getPurapDocumentIdentifier() + "(document id " + po.getDocumentNumber() + ")";
        String responsibilityNote = "Purchase Order Amendment Routed By User";

        for (AdHocRoutePerson adHocPerson : fyiList) {
            try {
                po.appSpecificRouteDocumentToUser(po.getDocumentHeader().getWorkflowDocument(), adHocPerson.getPerson().getPrincipalId(), annotation, responsibilityNote);
            } catch (WorkflowException e) {
                throw new RuntimeException("Error routing fyi for document with id " + po.getDocumentNumber(), e);
            }

        }
    }

    /**
     * Creates a list of fiscal officers for new unordered items added to a purchase order.
     *
     * @param po
     * @return
     */
    protected List<AdHocRoutePerson> createFyiFiscalOfficerListForNewUnorderedItems(PurchaseOrderDocument po) {

        List<AdHocRoutePerson> adHocRoutePersons = new ArrayList<AdHocRoutePerson>();
        Map fiscalOfficers = new HashMap();
        AdHocRoutePerson adHocRoutePerson = null;

        for (PurchaseOrderItem poItem : (List<PurchaseOrderItem>) po.getItems()) {
            // only check, active, above the line, unordered items
            if (poItem.isItemActiveIndicator() && poItem.getItemType().isLineItemIndicator() && PurapConstants.ItemTypeCodes.ITEM_TYPE_UNORDERED_ITEM_CODE.equals(poItem.getItemTypeCode())) {

                // if the item identifier is null its new, or if the item doesn't exist on the current purchase order it's new
                if (poItem.getItemIdentifier() == null || !purchaseOrderDao.itemExistsOnPurchaseOrder(poItem.getItemLineNumber(), purchaseOrderDao.getDocumentNumberForCurrentPurchaseOrder(po.getPurapDocumentIdentifier()))) {

                    // loop through accounts and pull off fiscal officer
                    for (PurApAccountingLine account : poItem.getSourceAccountingLines()) {

                        // check for dupes of fiscal officer
                        if (fiscalOfficers.containsKey(account.getAccount().getAccountFiscalOfficerUser().getPrincipalName()) == false) {

                            // add fiscal officer to list
                            fiscalOfficers.put(account.getAccount().getAccountFiscalOfficerUser().getPrincipalName(), account.getAccount().getAccountFiscalOfficerUser().getPrincipalName());

                            // create AdHocRoutePerson object and add to list
                            adHocRoutePerson = new AdHocRoutePerson();
                            adHocRoutePerson.setId(account.getAccount().getAccountFiscalOfficerUser().getPrincipalName());
                            adHocRoutePerson.setActionRequested(KFSConstants.WORKFLOW_FYI_REQUEST);
                            adHocRoutePersons.add(adHocRoutePerson);
                        }
                    }
                }
            }
        }

        return adHocRoutePersons;
    }


    /**
     * Sends an FYI to fiscal officers for general ledger entries created for amend purchase order
     *
     * @param po
     */
    @Override
    public void sendFyiForGLEntries(PurchaseOrderDocument po) {

        List<AdHocRoutePerson> fyiList = createFyiFiscalOfficerListForAmendGlEntries(po);
        String annotation = "Amendment to Purchase Order " + po.getPurapDocumentIdentifier() + "( Document id " + po.getDocumentNumber() + ")" + " resulted in the generation of Pending General Ledger Entries.";
        String responsibilityNote = "Purchase Order Amendment Routed By User";

        for (AdHocRoutePerson adHocPerson : fyiList) {
            try {
                po.appSpecificRouteDocumentToUser(po.getDocumentHeader().getWorkflowDocument(), adHocPerson.getPerson().getPrincipalId(), annotation, responsibilityNote);
            } catch (WorkflowException e) {
                throw new RuntimeException("Error routing fyi for document with id " + po.getDocumentNumber(), e);
            }

        }
    }

    @Override
    public void sendAdhocFyi(PurchaseOrderDocument po) {

        RequisitionDocument req = po.getPurApSourceDocumentIfPossible();

        String reqInitiator = null;

        if (ObjectUtils.isNotNull(req)) {
            reqInitiator = req.getDocumentHeader().getWorkflowDocument().getInitiatorPrincipalId();
        }

        String currentDocumentTypeName = po.getDocumentHeader().getWorkflowDocument().getDocumentTypeName();
        Set<String> fiscalOfficerIds = new HashSet<String>();
        Set<Account> accounts = new HashSet<Account>();
        try {
            if (reqInitiator != null) {
                po.appSpecificRouteDocumentToUser(po.getDocumentHeader().getWorkflowDocument(), reqInitiator, getAdhocFyiAnnotation(po) + KFSConstants.BLANK_SPACE + req.getPurapDocumentIdentifier() + KFSConstants.BLANK_SPACE + "(document Id " + req.getDocumentNumber() + ")", "Requisition Routed By User");
            }

            if (!PurapConstants.PurchaseOrderDocTypes.PURCHASE_ORDER_AMENDMENT_DOCUMENT.equalsIgnoreCase(currentDocumentTypeName)) {
                List<PurchaseOrderItem> items = po.getItemsActiveOnly();
                for (PurchaseOrderItem item : items) {
                    List<PurApAccountingLine> lines = item.getSourceAccountingLines();
                    for (PurApAccountingLine line : lines) {
                        accounts.add(line.getAccount());
                    }
                }
                for (Account account : accounts) {
                    String principalId = account.getAccountFiscalOfficerUser().getPrincipalId();

                    if (!fiscalOfficerIds.contains(principalId)) {
                        fiscalOfficerIds.add(principalId);
                        AccountDelegate accountDelegate = getAccountPrimaryDelegate(account);
                        if (ObjectUtils.isNotNull(accountDelegate)) {
                            String delegateName = KimApiServiceLocator.getPersonService().getPerson(accountDelegate.getAccountDelegateSystemId()).getPrincipalName();
                            String annotationText = "Delegation of: " + KFSConstants.CoreModuleNamespaces.KFS + KFSConstants.BLANK_SPACE + KFSConstants.SysKimApiConstants.FISCAL_OFFICER_KIM_ROLE_NAME + KFSConstants.BLANK_SPACE + account.getChartOfAccountsCode() + KFSConstants.BLANK_SPACE + account.getAccountNumber() + KFSConstants.BLANK_SPACE + "to principal" + KFSConstants.BLANK_SPACE + delegateName;
                            po.appSpecificRouteDocumentToUser(po.getDocumentHeader().getWorkflowDocument(), accountDelegate.getAccountDelegateSystemId(), annotationText, "Fiscal Officer Notification");
                        } else {
                            String annotationText = KFSConstants.CoreModuleNamespaces.KFS + KFSConstants.BLANK_SPACE + KFSConstants.SysKimApiConstants.FISCAL_OFFICER_KIM_ROLE_NAME + KFSConstants.BLANK_SPACE + account.getChartOfAccountsCode() + KFSConstants.BLANK_SPACE + account.getAccountNumber();
                            po.appSpecificRouteDocumentToUser(po.getDocumentHeader().getWorkflowDocument(), principalId, annotationText, "Fiscal Officer Notification");
                        }


                    }
                }
            }


        } catch (WorkflowException ex) {
            throw new RuntimeException("Error routing fyi for document with id " + po.getDocumentNumber(), ex);
        }

    }

    private AccountDelegate getAccountPrimaryDelegate(Account account) {
        AccountDelegate delegateExample = new AccountDelegate();
        delegateExample.setChartOfAccountsCode(account.getChartOfAccountsCode());
        delegateExample.setAccountNumber(account.getAccountNumber());
        delegateExample.setFinancialDocumentTypeCode(PurapConstants.PurchaseOrderDocTypes.PURCHASE_ORDER_DOCUMENT);
        AccountDelegate accountDelegate = SpringContext.getBean(AccountService.class).getPrimaryDelegationByExample(delegateExample, null);
        return accountDelegate;

    }

    protected String getAdhocFyiAnnotation(PurchaseOrderDocument po) {
        String annotation = "";
        if (po.getDocumentHeader().getWorkflowDocument().isDisapproved()) {
            annotation = KRADServiceLocator.getKualiConfigurationService().getPropertyValueAsString(PurapConstants.PO_DISAPPROVAL_ANNOTATION_TEXT);
        }
        if (po.getDocumentHeader().getWorkflowDocument().isFinal()) {
            annotation = KRADServiceLocator.getKualiConfigurationService().getPropertyValueAsString(PurapConstants.PO_FINAL_ANNOTATION_TEXT);
        }
        if (po.getDocumentHeader().getWorkflowDocument().isCanceled()) {
            annotation = KRADServiceLocator.getKualiConfigurationService().getPropertyValueAsString(PurapConstants.PO_CANCEL_ANNOTATION_TEXT);
        }
        return annotation;
    }

    /**
     * Creates a list of fiscal officers for amend genera
     *
     * @param po
     * @return
     */
    protected List<AdHocRoutePerson> createFyiFiscalOfficerListForAmendGlEntries(PurchaseOrderDocument po) {

        List<AdHocRoutePerson> adHocRoutePersons = new ArrayList<AdHocRoutePerson>();
        Map fiscalOfficers = new HashMap();
        AdHocRoutePerson adHocRoutePerson = null;

        for (SourceAccountingLine account : po.getGlOnlySourceAccountingLines()) {
            // loop through accounts and pull off fiscal officer
            // for(PurApAccountingLine account : poItem.getSourceAccountingLines()){
            // check for dupes of fiscal officer
            Account acct = SpringContext.getBean(AccountService.class).getByPrimaryId(account.getChartOfAccountsCode(), account.getAccountNumber());
            String principalName = acct.getAccountFiscalOfficerUser().getPrincipalName();
            // String principalName = account.getAccount().getAccountFiscalOfficerUser().getPrincipalName();
            if (fiscalOfficers.containsKey(principalName) == false) {
                // add fiscal officer to list
                fiscalOfficers.put(principalName, principalName);
                // create AdHocRoutePerson object and add to list
                adHocRoutePerson = new AdHocRoutePerson();
                adHocRoutePerson.setId(principalName);
                adHocRoutePerson.setActionRequested(KewApiConstants.ACTION_REQUEST_FYI_REQ);
                adHocRoutePersons.add(adHocRoutePerson);
            }
            // }
        }

        return adHocRoutePersons;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#categorizeItemsForSplit(java.util.List)
     */
    @Override
    public HashMap<String, List<PurchaseOrderItem>> categorizeItemsForSplit(List<PurchaseOrderItem> items) {
        HashMap<String, List<PurchaseOrderItem>> movingOrNot = new HashMap<String, List<PurchaseOrderItem>>(3);
        List<PurchaseOrderItem> movingPOItems = new ArrayList<PurchaseOrderItem>();
        List<PurchaseOrderItem> remainingPOItems = new ArrayList<PurchaseOrderItem>();
        List<PurchaseOrderItem> remainingPOLineItems = new ArrayList<PurchaseOrderItem>();
        for (PurchaseOrderItem item : items) {
            if (item.isMovingToSplit()) {
                movingPOItems.add(item);
            } else {
                remainingPOItems.add(item);
                if (item.getItemType().isLineItemIndicator()) {
                    remainingPOLineItems.add(item);
                }
            }
        }
        movingOrNot.put(PODocumentsStrings.ITEMS_MOVING_TO_SPLIT, movingPOItems);
        movingOrNot.put(PODocumentsStrings.ITEMS_REMAINING, remainingPOItems);
        movingOrNot.put(PODocumentsStrings.LINE_ITEMS_REMAINING, remainingPOLineItems);
        return movingOrNot;
    }

    /**
     * @see org.kuali.module.purap.service.PurchaseOrderService#populateQuoteWithVendor(java.lang.Integer, java.lang.Integer,
     * java.lang.String)
     */
    @Override
    public PurchaseOrderVendorQuote populateQuoteWithVendor(Integer headerId, Integer detailId, String documentNumber) {
        VendorDetail vendor = vendorService.getVendorDetail(headerId, detailId);
        updateDefaultVendorAddress(vendor);
        PurchaseOrderVendorQuote newPOVendorQuote = populateAddressForPOVendorQuote(vendor, documentNumber);

        // Set the vendorPhoneNumber on the quote to be the first "phone number" type phone
        // found on the list. If there's no "phone number" type found, the quote's
        // vendorPhoneNumber will be blank regardless of any other types of phone found on the list.
        for (VendorPhoneNumber phone : vendor.getVendorPhoneNumbers()) {
            if (VendorConstants.PhoneTypes.PHONE.equals(phone.getVendorPhoneTypeCode())) {
                newPOVendorQuote.setVendorPhoneNumber(phone.getVendorPhoneNumber());
                break;
            }
        }

        return newPOVendorQuote;
    }

    /**
     * Creates the new PurchaseOrderVendorQuote and populate the address fields for it.
     *
     * @param newVendor      The VendorDetail object from which we obtain the values for the address fields.
     * @param documentNumber The documentNumber of the PurchaseOrderDocument containing the PurchaseOrderVendorQuote.
     * @return
     */
    protected PurchaseOrderVendorQuote populateAddressForPOVendorQuote(VendorDetail newVendor, String documentNumber) {
        PurchaseOrderVendorQuote newPOVendorQuote = new PurchaseOrderVendorQuote();
        newPOVendorQuote.setVendorName(newVendor.getVendorName());
        newPOVendorQuote.setVendorHeaderGeneratedIdentifier(newVendor.getVendorHeaderGeneratedIdentifier());
        newPOVendorQuote.setVendorDetailAssignedIdentifier(newVendor.getVendorDetailAssignedIdentifier());
        newPOVendorQuote.setDocumentNumber(documentNumber);
        boolean foundAddress = false;
        for (VendorAddress address : newVendor.getVendorAddresses()) {
            if (AddressTypes.QUOTE.equals(address.getVendorAddressTypeCode())) {
                newPOVendorQuote.setVendorCityName(address.getVendorCityName());
                newPOVendorQuote.setVendorCountryCode(address.getVendorCountryCode());
                newPOVendorQuote.setVendorLine1Address(address.getVendorLine1Address());
                newPOVendorQuote.setVendorLine2Address(address.getVendorLine2Address());
                newPOVendorQuote.setVendorPostalCode(address.getVendorZipCode());
                newPOVendorQuote.setVendorStateCode(address.getVendorStateCode());
                newPOVendorQuote.setVendorFaxNumber(address.getVendorFaxNumber());
                foundAddress = true;
                break;
            }
        }
        if (!foundAddress) {
            newPOVendorQuote.setVendorCityName(newVendor.getDefaultAddressCity());
            newPOVendorQuote.setVendorCountryCode(newVendor.getDefaultAddressCountryCode());
            newPOVendorQuote.setVendorLine1Address(newVendor.getDefaultAddressLine1());
            newPOVendorQuote.setVendorLine2Address(newVendor.getDefaultAddressLine2());
            newPOVendorQuote.setVendorPostalCode(newVendor.getDefaultAddressPostalCode());
            newPOVendorQuote.setVendorStateCode(newVendor.getDefaultAddressStateCode());
            newPOVendorQuote.setVendorFaxNumber(newVendor.getDefaultFaxNumber());
        }
        return newPOVendorQuote;
    }

    /**
     * Obtains the defaultAddress of the vendor and setting the default address fields on the vendor.
     *
     * @param vendor The VendorDetail object whose default address we'll obtain and set the fields.
     */
    protected void updateDefaultVendorAddress(VendorDetail vendor) {

        VendorAddress defaultAddress = vendorService.getVendorDefaultAddress(vendor.getVendorHeaderGeneratedIdentifier(), vendor.getVendorDetailAssignedIdentifier(), vendor.getVendorHeader().getVendorType().getAddressType().getVendorAddressTypeCode(), "", false);
        if (defaultAddress != null) {
            if (defaultAddress.getVendorState() != null) {
                vendor.setVendorStateForLookup(defaultAddress.getVendorState().getName());
            }
            vendor.setDefaultAddressLine1(defaultAddress.getVendorLine1Address());
            vendor.setDefaultAddressLine2(defaultAddress.getVendorLine2Address());
            vendor.setDefaultAddressCity(defaultAddress.getVendorCityName());
            vendor.setDefaultAddressPostalCode(defaultAddress.getVendorZipCode());
            vendor.setDefaultAddressStateCode(defaultAddress.getVendorStateCode());
            vendor.setDefaultAddressInternationalProvince(defaultAddress.getVendorAddressInternationalProvinceName());
            vendor.setDefaultAddressCountryCode(defaultAddress.getVendorCountryCode());
            vendor.setDefaultFaxNumber(defaultAddress.getVendorFaxNumber());
        }
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#processACMReq(org.kuali.kfs.module.purap.document.ContractManagerAssignmentDocument)
     */
    @Override
    public void processACMReq(ContractManagerAssignmentDocument acmDoc) {
        List<ContractManagerAssignmentDetail> acmDetails = acmDoc.getContractManagerAssignmentDetails();
        for (Iterator iter = acmDetails.iterator(); iter.hasNext(); ) {
            ContractManagerAssignmentDetail detail = (ContractManagerAssignmentDetail) iter.next();

            if (ObjectUtils.isNotNull(detail.getContractManagerCode())) {
                // Get the requisition for this ContractManagerAssignmentDetail.
                RequisitionDocument req = requisitionService.getRequisitionById(detail.getRequisitionIdentifier());

                if (PurapConstants.RequisitionStatuses.APPDOC_AWAIT_CONTRACT_MANAGER_ASSGN.equals(req.getApplicationDocumentStatus())) {
                    // only update REQ if code is empty and status is correct
                    try {
                        req.updateAndSaveAppDocStatus(PurapConstants.RequisitionStatuses.APPDOC_CLOSED);
                    } catch (WorkflowException e) {
                        throw new RuntimeException("Error saving routing data while saving document with id " + req.getDocumentNumber(), e);
                    }

                    purapService.saveDocumentNoValidation(req);
                    createPurchaseOrderDocument(req, KFSConstants.SYSTEM_USER, detail.getContractManagerCode());
                }
            }

        }// endfor
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#autoCloseRecurringOrders()
     */
    @Override
    public boolean autoCloseRecurringOrders() {
        LOG.debug("autoCloseRecurringOrders() started");
        boolean shouldSendEmail = true;
        MailMessage message = new MailMessage();
        String parameterEmail = parameterService.getParameterValueAsString(AutoCloseRecurringOrdersStep.class, PurapParameterConstants.AUTO_CLOSE_RECURRING_PO_TO_EMAIL_ADDRESSES);

        if (StringUtils.isEmpty(parameterEmail)) {
            // Don't stop the show if the email address is wrong, log it and continue.
            LOG.error("autoCloseRecurringOrders(): parameterEmail is missing, we'll not send out any emails for this job.");
            shouldSendEmail = false;
        }
        if (shouldSendEmail) {
            message = setMessageAddressesAndSubject(message, parameterEmail);
        }
        StringBuffer emailBody = new StringBuffer();
        // There should always be a "AUTO_CLOSE_RECURRING_ORDER_DT"
        // row in the table, this method sets it to "mm/dd/yyyy" after processing.
        String recurringOrderDateString = parameterService.getParameterValueAsString(AutoCloseRecurringOrdersStep.class, PurapParameterConstants.AUTO_CLOSE_RECURRING_PO_DATE);
        boolean validDate = true;
        java.util.Date recurringOrderDate = null;
        try {
            recurringOrderDate = dateTimeService.convertToDate(recurringOrderDateString);
        } catch (ParseException pe) {
            validDate = false;
        }
        if (StringUtils.isEmpty(recurringOrderDateString) || recurringOrderDateString.equalsIgnoreCase("mm/dd/yyyy") || (!validDate)) {
            if (recurringOrderDateString.equalsIgnoreCase("mm/dd/yyyy")) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("autoCloseRecurringOrders(): mm/dd/yyyy " + "was found in the Application Settings table. No orders will be closed, method will end.");
                }
                if (shouldSendEmail) {
                    emailBody.append("The AUTO_CLOSE_RECURRING_ORDER_DT found in the Application Settings table " + "was mm/dd/yyyy. No recurring PO's were closed.");
                }
            } else {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("autoCloseRecurringOrders(): An invalid autoCloseRecurringOrdersDate " + "was found in the Application Settings table: " + recurringOrderDateString + ". Method will end.");
                }
                if (shouldSendEmail) {
                    emailBody.append("An invalid AUTO_CLOSE_RECURRING_ORDER_DT was found in the Application Settings table: " + recurringOrderDateString + ". No recurring PO's were closed.");
                }
            }
            if (shouldSendEmail) {
                sendMessage(message, emailBody.toString());
            }
            LOG.info("autoCloseRecurringOrders() ended");

            return false;
        }
        LOG.info("autoCloseRecurringOrders() The autoCloseRecurringOrdersDate found in the Application Settings table was " + recurringOrderDateString);
        if (shouldSendEmail) {
            emailBody.append("The autoCloseRecurringOrdersDate found in the Application Settings table was " + recurringOrderDateString + ".");
        }
        Calendar appSettingsDate = dateTimeService.getCalendar(recurringOrderDate);
        Timestamp appSettingsDay = new Timestamp(appSettingsDate.getTime().getTime());

        Calendar todayMinusThreeMonths = getTodayMinusThreeMonths();
        Timestamp threeMonthsAgo = new Timestamp(todayMinusThreeMonths.getTime().getTime());

        if (appSettingsDate.after(todayMinusThreeMonths)) {
            LOG.info("autoCloseRecurringOrders() The appSettingsDate: " + appSettingsDay + " is after todayMinusThreeMonths: " + threeMonthsAgo + ". The program will end.");
            if (shouldSendEmail) {
                emailBody.append("\n\nThe autoCloseRecurringOrdersDate: " + appSettingsDay + " is after todayMinusThreeMonths: " + threeMonthsAgo + ". The program will end.");
                sendMessage(message, emailBody.toString());
            }
            LOG.info("autoCloseRecurringOrders() ended");

            return false;
        }

        List<AutoClosePurchaseOrderView> closeList = purchaseOrderDao.getAutoCloseRecurringPurchaseOrders(getExcludedVendorChoiceCodes());

        // we need to eliminate the AutoClosePurchaseOrderView whose workflowdocument status is not OPEN..
        // KFSMI-7533
        List<AutoClosePurchaseOrderView> purchaseOrderAutoCloseList = filterDocumentsForAppDocStatusOpen(closeList);

        LOG.info("autoCloseRecurringOrders(): " + purchaseOrderAutoCloseList.size() + " PO's were returned for processing.");
        int counter = 0;
        for (AutoClosePurchaseOrderView poAutoClose : purchaseOrderAutoCloseList) {
            LOG.info("autoCloseRecurringOrders(): Testing PO ID " + poAutoClose.getPurapDocumentIdentifier() + ". recurringPaymentEndDate: " + poAutoClose.getRecurringPaymentEndDate());
            if (poAutoClose.getRecurringPaymentEndDate().before(threeMonthsAgo)) {
                String newStatus = PurapConstants.PurchaseOrderStatuses.APPDOC_PENDING_CLOSE;
                String annotation = "This recurring PO was automatically closed in batch.";
                String documentType = PurapConstants.PurchaseOrderDocTypes.PURCHASE_ORDER_CLOSE_DOCUMENT;
                PurchaseOrderDocument document = getPurchaseOrderByDocumentNumber(poAutoClose.getDocumentNumber());
                boolean rulePassed = kualiRuleService.applyRules(new AttributedRouteDocumentEvent("", document));

                boolean success = true;
                if (success) {
                    ++counter;
                    if (counter == 1) {
                        emailBody.append("\n\nThe following recurring Purchase Orders will be closed by auto close recurring batch job \n");
                    }
                    LOG.info("autoCloseRecurringOrders() PO ID " + poAutoClose.getPurapDocumentIdentifier() + " will be closed.");
                    createNoteForAutoCloseOrders(document, annotation);
                    createAndRoutePotentialChangeDocument(poAutoClose.getDocumentNumber(), documentType, annotation, null, newStatus);
                    if (shouldSendEmail) {
                        emailBody.append("\n\n" + counter + " PO ID: " + poAutoClose.getPurapDocumentIdentifier() + ", End Date: " + poAutoClose.getRecurringPaymentEndDate() + ", Status: " + poAutoClose.getApplicationDocumentStatus() + ", VendorChoice: " + poAutoClose.getVendorChoiceCode() + ", RecurringPaymentType: " + poAutoClose.getRecurringPaymentTypeCode());
                    }
                } else {
                    // If it was unsuccessful, we have to clear the error map in the GlobalVariables so that the previous
                    // error would not still be lingering around and the next PO in the list can be validated.
                    GlobalVariables.getMessageMap().clearErrorMessages();
                }
            }
        }
        if (counter == 0) {
            LOG.info("\n\nNo recurring PO's fit the conditions for closing.");
            if (shouldSendEmail) {
                emailBody.append("\n\nNo recurring PO's fit the conditions for closing.");
            }
        }
        if (shouldSendEmail) {
            sendMessage(message, emailBody.toString());
        }
        resetAutoCloseRecurringOrderDateParameter();
        LOG.debug("autoCloseRecurringOrders() ended");

        return true;
    }

    /**
     * Filter out the auto close purchase order view documents for the appDocStatus with status open For each document in the list,
     * check if there is workflowdocument whose appdocstatus is open add add to the return list.
     *
     * @param List<AutoClosePurchaseOrderView>
     * @param appDocStatus
     * @return filteredAutoClosePOView filtered auto close po view documents where appdocstatus is open
     */
    protected List<AutoClosePurchaseOrderView> filterDocumentsForAppDocStatusOpen(List<AutoClosePurchaseOrderView> autoClosePurchaseOrderViews) {
        List<AutoClosePurchaseOrderView> filteredAutoClosePOView = new ArrayList<AutoClosePurchaseOrderView>();

        for (AutoClosePurchaseOrderView autoClosePurchaseOrderView : autoClosePurchaseOrderViews) {
            Document document = findDocument(autoClosePurchaseOrderView.getDocumentNumber());

            if (document != null) {
                if (PurapConstants.PurchaseOrderStatuses.APPDOC_OPEN.equalsIgnoreCase(document.getDocumentHeader().getWorkflowDocument().getApplicationDocumentStatus())) {
                    // found the matched Awaiting Contract Manager Assignment status, retrieve the routeHeaderId and add to the list
                    filteredAutoClosePOView.add(autoClosePurchaseOrderView);
                }
            }
        }

        return filteredAutoClosePOView;
    }

    /**
     * This method finds the document for the given document header id
     *
     * @param documentHeaderId
     * @return document The document in the workflow that matches the document header id.
     */
    protected Document findDocument(String documentHeaderId) {
        Document document = null;

        try {
            document = documentService.getByDocumentHeaderId(documentHeaderId);
        } catch (WorkflowException ex) {
            LOG.error("Exception encountered on finding the document: " + documentHeaderId, ex);
        } catch (UnknownDocumentTypeException ex) {
            // don't blow up just because a document type is not installed (but don't return it either)
            LOG.error("Exception encountered on finding the document: " + documentHeaderId, ex);
        }

        return document;
    }


    /**
     * Creates and returns a Calendar object of today minus three months.
     *
     * @return Calendar object of today minus three months.
     */
    protected Calendar getTodayMinusThreeMonths() {
        Calendar todayMinusThreeMonths = Calendar.getInstance(); // Set to today.
        todayMinusThreeMonths.add(Calendar.MONTH, -3); // Back up 3 months.
        todayMinusThreeMonths.set(Calendar.HOUR, 12);
        todayMinusThreeMonths.set(Calendar.MINUTE, 0);
        todayMinusThreeMonths.set(Calendar.SECOND, 0);
        todayMinusThreeMonths.set(Calendar.MILLISECOND, 0);
        todayMinusThreeMonths.set(Calendar.AM_PM, Calendar.AM);
        return todayMinusThreeMonths;
    }

    /**
     * Sets the to addresses, from address and the subject of the email.
     *
     * @param message        The MailMessage object of the email to be sent.
     * @param parameterEmail The String of email addresses with delimiters of ";" obtained from the system parameter.
     * @return The MailMessage object after the to addresses, from address and the subject have been set.
     */
    protected MailMessage setMessageAddressesAndSubject(MailMessage message, String parameterEmail) {
        String toAddressList[] = parameterEmail.split(";");

        if (toAddressList.length > 0) {
            for (int i = 0; i < toAddressList.length; i++) {
                if (toAddressList[i] != null) {
                    message.addToAddress(toAddressList[i].trim());
                }
            }
        }

        message.setFromAddress(toAddressList[0]);
        message.setSubject("Auto Close Recurring Purchase Orders");
        return message;
    }

    /**
     * Sends the email by calling the sendMessage method in mailService and log error if exception occurs during the attempt to send
     * the message.
     *
     * @param message   The MailMessage object containing information to be sent.
     * @param emailBody The String containing the body of the email to be sent.
     */
    protected void sendMessage(MailMessage message, String emailBody) {
        message.setMessage(emailBody);
        try {
            mailService.sendMessage(message);
        } catch (Exception e) {
            // Don't stop the show if the email has problem, log it and continue.
            LOG.error("autoCloseRecurringOrders(): email problem. Message not sent.", e);
        }
    }

    /**
     * Resets the AUTO_CLOSE_RECURRING_ORDER_DT system parameter to "mm/dd/yyyy".
     */
    protected void resetAutoCloseRecurringOrderDateParameter() {
        Parameter autoCloseRecurringPODate = parameterService.getParameter(AutoCloseRecurringOrdersStep.class, PurapParameterConstants.AUTO_CLOSE_RECURRING_PO_DATE);
        if (autoCloseRecurringPODate != null) {
            Parameter.Builder updatedParameter = Parameter.Builder.create(autoCloseRecurringPODate);
            updatedParameter.setValue("mm/dd/yyyy");
            parameterService.updateParameter(updatedParameter.build());
        }
    }

    /**
     * Gets a List of excluded vendor choice codes from PurapConstants.
     *
     * @return a List of excluded vendor choice codes
     */
    protected List<String> getExcludedVendorChoiceCodes() {
        List<String> excludedVendorChoiceCodes = new ArrayList<String>();
        for (int i = 0; i < PurapConstants.AUTO_CLOSE_EXCLUSION_VNDR_CHOICE_CODES.length; i++) {
            String excludedCode = PurapConstants.AUTO_CLOSE_EXCLUSION_VNDR_CHOICE_CODES[i];
            excludedVendorChoiceCodes.add(excludedCode);
        }
        return excludedVendorChoiceCodes;
    }

    /**
     * Creates and add a note to the purchase order document using the annotation String in the input parameter. This method is used
     * by the autoCloseRecurringOrders() and autoCloseFullyDisencumberedOrders to add a note to the purchase order to indicate that
     * the purchase order was closed by the batch job.
     *
     * @param purchaseOrderDocument The purchase order document that is being closed by the batch job.
     * @param annotation            The string to appear on the note to be attached to the purchase order.
     */
    @Override
    public void createNoteForAutoCloseOrders(PurchaseOrderDocument purchaseOrderDocument, String annotation) {
        try {
            Note noteObj = documentService.createNoteFromDocument(purchaseOrderDocument, annotation);
            // documentService.addNoteToDocument(purchaseOrderDocument, noteObj);
            noteService.save(noteObj);
        } catch (Exception e) {
            String errorMessage = "Error creating and saving close note for purchase order with document service";
            LOG.error("createNoteForAutoCloseRecurringOrders " + errorMessage, e);
            throw new RuntimeException(errorMessage, e);
        }
    }

    @Override
    public List<AutoClosePurchaseOrderView> getAllOpenPurchaseOrdersForAutoClose() {
        return purchaseOrderDao.getAllOpenPurchaseOrders(getExcludedVendorChoiceCodes());
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#retrieveCapitalAssetItemsForIndividual(java.lang.Integer)
     */
    @Override
    public List<PurchasingCapitalAssetItem> retrieveCapitalAssetItemsForIndividual(Integer poId) {
        PurchaseOrderDocument po = getCurrentPurchaseOrder(poId);
        if (ObjectUtils.isNotNull(po)) {
            return po.getPurchasingCapitalAssetItems();
        }
        return null;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#retrieveCapitalAssetSystemForOneSystem(java.lang.Integer)
     */
    @Override
    public CapitalAssetSystem retrieveCapitalAssetSystemForOneSystem(Integer poId) {
        PurchaseOrderDocument po = getCurrentPurchaseOrder(poId);
        if (ObjectUtils.isNotNull(po)) {
            List<CapitalAssetSystem> systems = po.getPurchasingCapitalAssetSystems();
            if (ObjectUtils.isNotNull(systems)) {
                // for one system, there should only ever be one system
                return systems.get(0);
            }
        }
        return null;
    }

    /**
     * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#retrieveCapitalAssetSystemsForMultipleSystem(java.lang.Integer)
     */
    @Override
    public List<CapitalAssetSystem> retrieveCapitalAssetSystemsForMultipleSystem(Integer poId) {
        PurchaseOrderDocument po = getCurrentPurchaseOrder(poId);
        if (ObjectUtils.isNotNull(po)) {
            return po.getPurchasingCapitalAssetSystems();
        }
        return null;
    }

    /**
     * This method fixes the item references in this document
     */
    protected void fixItemReferences(PurchaseOrderDocument po) {
        // fix item and account references in case this is a new doc (since they will be lost)
        for (PurApItem item : (List<PurApItem>) po.getItems()) {
            item.setPurapDocument(po);
            item.fixAccountReferences();
        }
    }

    @Override
    public List getPendingPurchaseOrderFaxes() {
        List<PurchaseOrderDocument> purchaseOrderList = purchaseOrderDao.getPendingPurchaseOrdersForFaxing();
        return filterPurchaseOrderDocumentByAppDocStatus(purchaseOrderList, PurapConstants.PurchaseOrderStatuses.APPDOC_PENDING_FAX);
    }

    /**
     * This method queries financialSystemDocumentHeader and filter payment requests against the provided status.
     *
     * @param paymentRequestDocuments
     * @param appDocStatus
     * @return
     */
    protected List<PurchaseOrderDocument> filterPurchaseOrderDocumentByAppDocStatus(Collection<PurchaseOrderDocument> purchaseOrderDocuments, String... appDocStatus) {
        List<String> appDocStatusList = Arrays.asList(appDocStatus);
        List<PurchaseOrderDocument> filteredPaymentRequestDocuments = new ArrayList<PurchaseOrderDocument>();
        // add to filtered collection if the app doc list contains payment request's application document status.
        for (PurchaseOrderDocument po : purchaseOrderDocuments) {
            if (appDocStatusList.contains(po.getApplicationDocumentStatus())) {
                filteredPaymentRequestDocuments.add(po);
            }
        }
        return filteredPaymentRequestDocuments;
    }

    /**
     * Query appDocStatus using financialSystemDocumentService and filter against the provided status list
     *
     * @param lookupDocNumbers
     * @param appDocStatus
     * @return List<String> purchaseOrderDocumentNumbers
     */
    @Deprecated
    protected List<String> filterPurchaseOrderDocumentNumbersByAppDocStatus(List<String> lookupDocNumbers, String... appDocStatus) {
        List<String> purchaseOrderDocNumbers = new ArrayList<String>();
        List<String> appDocStatusList = Arrays.asList(appDocStatus);
        for (String docNumber : lookupDocNumbers) {
            if (appDocStatusList.contains(financialSystemDocumentService.findByDocumentNumber(docNumber).getApplicationDocumentStatus())) {
                purchaseOrderDocNumbers.add(docNumber);
            }
        }
        return purchaseOrderDocNumbers;
    }

    @Override
    public String getPurchaseOrderAppDocStatus(Integer poId) {
        // TODO: This could be kind of expensive for one field
        PurchaseOrderDocument po = getCurrentPurchaseOrder(poId);
        if (ObjectUtils.isNotNull(po)) {
            return po.getApplicationDocumentStatus();
        }

        return null;
    }

    /**
     * helper method to take the po and save it using businessObjectService so that only the po related data is saved since most
     * often we are only updating the flags on the document. It will then reindex the document.
     *
     * @param po
     */
    protected void savePurchaseOrderData(PurchaseOrderDocument po) {
        // saving old PO using the business object service because the documentService saveDocument
        // will try to save the notes again and will cause ojb lock exception.
        // since only values that is changed on PO is pendingActionIndicator, save on businessObjectService is used
        // KFSMI-9741

        businessObjectService.save(po);

        // reindex the document so that the app doc status gets updated in the results for the PO lookups.
        final DocumentAttributeIndexingQueue documentAttributeIndexingQueue = KewApiServiceLocator.getDocumentAttributeIndexingQueue();
        documentAttributeIndexingQueue.indexDocument(po.getDocumentNumber());

    }

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

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

    public void setB2bPurchaseOrderService(B2BPurchaseOrderService purchaseOrderService) {
        this.b2bPurchaseOrderService = purchaseOrderService;
    }

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

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

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

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

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

    public void setPrintService(PrintService printService) {
        this.printService = printService;
    }

    public void setPurchaseOrderDao(PurchaseOrderDao purchaseOrderDao) {
        this.purchaseOrderDao = purchaseOrderDao;
    }

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

    public void setConfigurationService(ConfigurationService kualiConfigurationService) {
        this.kualiConfigurationService = kualiConfigurationService;
    }

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

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

    public void setRequisitionService(RequisitionService requisitionService) {
        this.requisitionService = requisitionService;
    }

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

    public void setMaintenanceDocumentService(MaintenanceDocumentService maintenanceDocumentService) {
        this.maintenanceDocumentService = maintenanceDocumentService;
    }

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

    public void setMailService(MailService mailService) {
        this.mailService = mailService;
    }

    public void setFinancialSystemDocumentService(FinancialSystemDocumentService financialSystemDocumentService) {
        this.financialSystemDocumentService = financialSystemDocumentService;
    }
}
