/*
 * 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.bc.document.service.impl;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.A21SubAccount;
import org.kuali.kfs.coa.businessobject.Account;
import org.kuali.kfs.coa.businessobject.SubAccount;
import org.kuali.kfs.coa.businessobject.SubFundGroup;
import org.kuali.kfs.coa.service.OrganizationService;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.fp.service.FiscalYearFunctionControlService;
import org.kuali.kfs.integration.ld.LaborLedgerBenefitsCalculation;
import org.kuali.kfs.integration.ld.LaborLedgerObject;
import org.kuali.kfs.kns.util.KNSGlobalVariables;
import org.kuali.kfs.kns.util.MessageList;
import org.kuali.kfs.krad.bo.AdHocRouteRecipient;
import org.kuali.kfs.krad.dao.DocumentDao;
import org.kuali.kfs.krad.document.Document;
import org.kuali.kfs.krad.exception.ValidationException;
import org.kuali.kfs.krad.rules.rule.event.KualiDocumentEvent;
import org.kuali.kfs.krad.rules.rule.event.SaveDocumentEvent;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.service.DocumentService;
import org.kuali.kfs.krad.service.KualiModuleService;
import org.kuali.kfs.krad.service.PersistenceService;
import org.kuali.kfs.krad.service.SessionDocumentService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.bc.BCConstants;
import org.kuali.kfs.module.bc.BCConstants.MonthSpreadDeleteType;
import org.kuali.kfs.module.bc.BCKeyConstants;
import org.kuali.kfs.module.bc.BCPropertyConstants;
import org.kuali.kfs.module.bc.businessobject.BudgetConstructionAccountOrganizationHierarchy;
import org.kuali.kfs.module.bc.businessobject.BudgetConstructionAccountReports;
import org.kuali.kfs.module.bc.businessobject.BudgetConstructionHeader;
import org.kuali.kfs.module.bc.businessobject.BudgetConstructionMonthly;
import org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding;
import org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionGeneralLedger;
import org.kuali.kfs.module.bc.businessobject.SalarySettingExpansion;
import org.kuali.kfs.module.bc.document.BudgetConstructionDocument;
import org.kuali.kfs.module.bc.document.dataaccess.BudgetConstructionDao;
import org.kuali.kfs.module.bc.document.service.BenefitsCalculationService;
import org.kuali.kfs.module.bc.document.service.BudgetDocumentService;
import org.kuali.kfs.module.bc.document.service.BudgetParameterService;
import org.kuali.kfs.module.bc.document.validation.event.DeleteMonthlySpreadEvent;
import org.kuali.kfs.module.bc.document.validation.impl.BudgetConstructionRuleUtil;
import org.kuali.kfs.module.bc.document.web.struts.BudgetConstructionForm;
import org.kuali.kfs.module.bc.document.web.struts.MonthlyBudgetForm;
import org.kuali.kfs.module.bc.util.BudgetParameterFinder;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSParameterKeyConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.FinancialSystemDocumentHeader;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.service.NonTransactional;
import org.kuali.kfs.sys.service.OptionsService;
import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.core.api.util.type.KualiInteger;
import org.kuali.rice.kew.api.document.WorkflowDocumentService;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kim.api.identity.Person;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

/**
 * Implements the BudgetDocumentService interface. Methods here operate on objects associated with the Budget Construction document
 * such as BudgetConstructionHeader
 */
public class BudgetDocumentServiceImpl implements BudgetDocumentService {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BudgetDocumentServiceImpl.class);

    protected BudgetConstructionDao budgetConstructionDao;
    protected DocumentDao documentDao;
    protected DocumentService documentService;
    protected WorkflowDocumentService workflowDocumentService;
    protected BenefitsCalculationService benefitsCalculationService;
    protected BusinessObjectService businessObjectService;
    protected KualiModuleService kualiModuleService;
    protected ParameterService parameterService;
    protected BudgetParameterService budgetParameterService;
    protected FiscalYearFunctionControlService fiscalYearFunctionControlService;
    protected OptionsService optionsService;
    protected PersistenceService persistenceService;
    protected OrganizationService organizationService;
    protected String defaultLaborBenefitRateCategoryCode;

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#getByCandidateKey(java.lang.String, java.lang.String,
     * java.lang.String, java.lang.Integer)
     */
    @Transactional
    public BudgetConstructionHeader getByCandidateKey(String chartOfAccountsCode, String accountNumber, String subAccountNumber, Integer fiscalYear) {
        return budgetConstructionDao.getByCandidateKey(chartOfAccountsCode, accountNumber, subAccountNumber, fiscalYear);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#saveDocument(org.kuali.rice.krad.document.Document)
     * similar to DocumentService.saveDocument()
     */
    @Transactional
    public Document saveDocument(BudgetConstructionDocument budgetConstructionDocument) throws WorkflowException, ValidationException {

        // user did explicit save here so mark as touched
        budgetConstructionDocument.getFinancialSystemDocumentHeader().setFinancialDocumentStatusCode(KFSConstants.DocumentStatusCodes.ENROUTE);

        this.saveDocumentNoWorkflow(budgetConstructionDocument);

        SpringContext.getBean(SessionDocumentService.class).addDocumentToUserSession(GlobalVariables.getUserSession(), budgetConstructionDocument.getDocumentHeader().getWorkflowDocument());

        // save any messages up to this point and put them back in after logDocumentAction()
        // this is a hack to get around the problem where messageLists gets cleared
        // that is PostProcessorServiceImpl.doActionTaken(ActionTakenEventDTO), establishGlobalVariables(), which does
        // GlobalVariables.clear()
        // not sure why this doesn't trash the GlobalVariables.getMessageMap()
        MessageList messagesSoFar = KNSGlobalVariables.getMessageList();

        budgetConstructionDocument.getDocumentHeader().getWorkflowDocument().logAnnotation("Document Updated");

        // putting messages back in
        KNSGlobalVariables.getMessageList().addAll(messagesSoFar);

        return budgetConstructionDocument;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#saveDocumentNoWorkflow(org.kuali.rice.krad.document.Document)
     */
    @Transactional
    public Document saveDocumentNoWorkflow(BudgetConstructionDocument bcDoc) throws ValidationException {
        return this.saveDocumentNoWorkFlow(bcDoc, MonthSpreadDeleteType.NONE, true);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#saveDocumentNoWorkFlow(org.kuali.kfs.module.bc.document.BudgetConstructionDocument,
     * org.kuali.kfs.module.bc.BCConstants.MonthSpreadDeleteType, boolean)
     */
    @Transactional
    public Document saveDocumentNoWorkFlow(BudgetConstructionDocument bcDoc, MonthSpreadDeleteType monthSpreadDeleteType, boolean doMonthRICheck) throws ValidationException {

        checkForNulls(bcDoc);

        bcDoc.prepareForSave();

        // validate and save the local objects not workflow objects
        // this eventually calls BudgetConstructionRules.processSaveDocument() which overrides the method in DocumentRuleBase
        if (doMonthRICheck) {
            validateAndPersistDocument(bcDoc, new SaveDocumentEvent(bcDoc));
        } else {
            validateAndPersistDocument(bcDoc, new DeleteMonthlySpreadEvent(bcDoc, monthSpreadDeleteType));
        }
        return bcDoc;
    }

    @Transactional
    public void saveMonthlyBudget(MonthlyBudgetForm monthlyBudgetForm, BudgetConstructionMonthly budgetConstructionMonthly) {

        BudgetConstructionForm budgetConstructionForm = (BudgetConstructionForm) GlobalVariables.getUserSession().retrieveObject(monthlyBudgetForm.getReturnFormKey());
        BudgetConstructionDocument bcDoc = budgetConstructionForm.getBudgetConstructionDocument();

        // handle any override situation
        // getting here assumes that the line is not a salary detail line and that overrides are allowed
        KualiInteger changeAmount = KualiInteger.ZERO;
        KualiInteger monthTotalAmount = budgetConstructionMonthly.getFinancialDocumentMonthTotalLineAmount();
        KualiInteger pbglRequestAmount = budgetConstructionMonthly.getPendingBudgetConstructionGeneralLedger().getAccountLineAnnualBalanceAmount();
        if (!monthTotalAmount.equals(pbglRequestAmount)) {

            changeAmount = monthTotalAmount.subtract(pbglRequestAmount);

            // change the pbgl request amount store it and sync the object in session
            budgetConstructionMonthly.refreshReferenceObject("pendingBudgetConstructionGeneralLedger");

            PendingBudgetConstructionGeneralLedger sourceRow = (PendingBudgetConstructionGeneralLedger) businessObjectService.retrieve(budgetConstructionMonthly.getPendingBudgetConstructionGeneralLedger());
            sourceRow.setAccountLineAnnualBalanceAmount(monthTotalAmount);
            businessObjectService.save(sourceRow);

            this.addOrUpdatePBGLRow(bcDoc, sourceRow, monthlyBudgetForm.isRevenue());
            if (monthlyBudgetForm.isRevenue()) {
                bcDoc.setRevenueAccountLineAnnualBalanceAmountTotal(bcDoc.getRevenueAccountLineAnnualBalanceAmountTotal().add(changeAmount));
            } else {
                bcDoc.setExpenditureAccountLineAnnualBalanceAmountTotal(bcDoc.getExpenditureAccountLineAnnualBalanceAmountTotal().add(changeAmount));

            }
        }

        businessObjectService.save(budgetConstructionMonthly);
        this.callForBenefitsCalcIfNeeded(bcDoc, budgetConstructionMonthly, changeAmount);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#callForBenefitsCalcIfNeeded(org.kuali.kfs.module.bc.document.BudgetConstructionDocument,
     * org.kuali.kfs.module.bc.businessobject.BudgetConstructionMonthly, org.kuali.rice.core.api.util.type.KualiInteger)
     */
    @Transactional
    public void callForBenefitsCalcIfNeeded(BudgetConstructionDocument bcDoc, BudgetConstructionMonthly budgetConstructionMonthly, KualiInteger pbglChangeAmount) {

        if (!benefitsCalculationService.isBenefitsCalculationDisabled()) {
            if (budgetConstructionMonthly.getPendingBudgetConstructionGeneralLedger().getPositionObjectBenefit() != null && !budgetConstructionMonthly.getPendingBudgetConstructionGeneralLedger().getPositionObjectBenefit().isEmpty()) {

                bcDoc.setMonthlyBenefitsCalcNeeded(true);
                if (pbglChangeAmount.isNonZero()) {
                    bcDoc.setBenefitsCalcNeeded(true);
                }
            }
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#calculateBenefitsIfNeeded(org.kuali.kfs.module.bc.document.BudgetConstructionDocument)
     */
    @Transactional
    public void calculateBenefitsIfNeeded(BudgetConstructionDocument bcDoc) {

        if (bcDoc.isBenefitsCalcNeeded() || bcDoc.isMonthlyBenefitsCalcNeeded()) {

            if (bcDoc.isBenefitsCalcNeeded()) {
                this.calculateAnnualBenefits(bcDoc);
            }

            if (bcDoc.isMonthlyBenefitsCalcNeeded()) {
                this.calculateMonthlyBenefits(bcDoc);
            }

            // reload from the DB and refresh refs
            this.reloadBenefitsLines(bcDoc);
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#calculateBenefits(org.kuali.kfs.module.bc.document.BudgetConstructionDocument)
     */
    @Transactional
    public void calculateBenefits(BudgetConstructionDocument bcDoc) {

        this.calculateAnnualBenefits(bcDoc);
        this.calculateMonthlyBenefits(bcDoc);

        // reload from the DB and refresh refs
        this.reloadBenefitsLines(bcDoc);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#calculateAnnualBenefits(org.kuali.kfs.module.bc.document.BudgetConstructionDocument)
     */
    @Transactional
    protected void calculateAnnualBenefits(BudgetConstructionDocument bcDoc) {

        // allow benefits calculation if document's account is not salary setting only lines
        bcDoc.setBenefitsCalcNeeded(false);
        if (!bcDoc.isSalarySettingOnly()) {

            // pbgl lines are saved at this point, calc benefits
            String sysParam = SpringContext.getBean(ParameterService.class).getParameterValueAsString(KfsParameterConstants.FINANCIAL_SYSTEM_ALL.class, KFSParameterKeyConstants.LdParameterConstants.ENABLE_FRINGE_BENEFIT_CALC_BY_BENEFIT_RATE_CATEGORY_IND);
            LOG.debug("sysParam: " + sysParam);
            // if sysParam == Y then Labor Benefit Rate Category Code must be used
            if (sysParam.equalsIgnoreCase("Y")) {
                benefitsCalculationService.calculateAnnualBudgetConstructionGeneralLedgerBenefits(bcDoc.getDocumentNumber(), bcDoc.getUniversityFiscalYear(), bcDoc.getChartOfAccountsCode(), bcDoc.getAccountNumber(), bcDoc.getSubAccountNumber(), bcDoc.getAccount().getLaborBenefitRateCategoryCode());
            } else {
                // rate category code off - call original benefits calc
                benefitsCalculationService.calculateAnnualBudgetConstructionGeneralLedgerBenefits(bcDoc.getDocumentNumber(), bcDoc.getUniversityFiscalYear(), bcDoc.getChartOfAccountsCode(), bcDoc.getAccountNumber(), bcDoc.getSubAccountNumber());
            }

            // write global message on calc success
            KNSGlobalVariables.getMessageList().add(BCKeyConstants.MESSAGE_BENEFITS_CALCULATED);
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#calculateMonthlyBenefits(org.kuali.kfs.module.bc.document.BudgetConstructionDocument)
     */
    @Transactional
    protected void calculateMonthlyBenefits(BudgetConstructionDocument bcDoc) {

        // allow benefits calculation if document's account is not salary setting only lines
        bcDoc.setMonthlyBenefitsCalcNeeded(false);
        if (!bcDoc.isSalarySettingOnly()) {

            // pbgl lines are saved at this point, calc benefits
            String sysParam = SpringContext.getBean(ParameterService.class).getParameterValueAsString(KfsParameterConstants.FINANCIAL_SYSTEM_ALL.class, KFSParameterKeyConstants.LdParameterConstants.ENABLE_FRINGE_BENEFIT_CALC_BY_BENEFIT_RATE_CATEGORY_IND);
            LOG.debug("sysParam: " + sysParam);
            // if sysParam == Y then Labor Benefit Rate Category Code must be used
            if (sysParam.equalsIgnoreCase("Y")) {
                benefitsCalculationService.calculateMonthlyBudgetConstructionGeneralLedgerBenefits(bcDoc.getDocumentNumber(), bcDoc.getUniversityFiscalYear(), bcDoc.getChartOfAccountsCode(), bcDoc.getAccountNumber(), bcDoc.getSubAccountNumber(), bcDoc.getAccount().getLaborBenefitRateCategoryCode());
            } else {
                // rate category code off - call original benefits calc
                benefitsCalculationService.calculateMonthlyBudgetConstructionGeneralLedgerBenefits(bcDoc.getDocumentNumber(), bcDoc.getUniversityFiscalYear(), bcDoc.getChartOfAccountsCode(), bcDoc.getAccountNumber(), bcDoc.getSubAccountNumber());
            }

            // write global message on calc success
            KNSGlobalVariables.getMessageList().add(BCKeyConstants.MESSAGE_BENEFITS_MONTHLY_CALCULATED);
        }
    }

    /**
     * Does sanity checks for null document object and null documentNumber
     *
     * @param document
     */
    @NonTransactional
    protected void checkForNulls(Document document) {
        if (document == null) {
            throw new IllegalArgumentException("invalid (null) document");
        } else if (document.getDocumentNumber() == null) {
            throw new IllegalStateException("invalid (null) documentHeaderId");
        }
    }

    /**
     * Runs validation and persists a document to the database.
     *
     * @param document
     * @param event
     * @throws WorkflowException
     * @throws ValidationException
     */
    @Transactional
    public void validateAndPersistDocument(Document document, KualiDocumentEvent event) throws ValidationException {
        if (document == null) {
            LOG.error("document passed to validateAndPersist was null");
            throw new IllegalArgumentException("invalid (null) document");
        }
        LOG.info("validating and preparing to persist document " + document.getDocumentNumber());

        // runs business rules event.validate() and creates rule instance and runs rule method recursively
        document.validateBusinessRules(event);

        // calls overriden method for specific document for anything that needs to happen before the save
        // currently nothing for BC document
        document.prepareForSave(event);

        // save the document to the database
        try {
            LOG.info("storing document " + document.getDocumentNumber());
            documentDao.save(document);
        } catch (OptimisticLockingFailureException e) {
            LOG.error("exception encountered on store of document " + e.getMessage());
            throw e;
        }

        document.postProcessSave(event);


    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#validateDocument(org.kuali.rice.krad.document.Document)
     */
    @Transactional
    public void validateDocument(Document document) throws ValidationException {
        if (document == null) {
            LOG.error("document passed to validateDocument was null");
            throw new IllegalArgumentException("invalid (null) document");
        }
        LOG.info("validating document " + document.getDocumentNumber());
        document.validateBusinessRules(new SaveDocumentEvent(document));

    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#getPBGLSalarySettingRows(org.kuali.kfs.module.bc.document.BudgetConstructionDocument)
     */
    @Transactional
    public List<PendingBudgetConstructionGeneralLedger> getPBGLSalarySettingRows(BudgetConstructionDocument bcDocument) {

        List<String> ssObjects = getDetailSalarySettingLaborObjects(bcDocument.getUniversityFiscalYear(), bcDocument.getChartOfAccountsCode());
        ssObjects.add(KFSConstants.BudgetConstructionConstants.OBJECT_CODE_2PLG);
        List<PendingBudgetConstructionGeneralLedger> pbglSalarySettingRows = budgetConstructionDao.getPBGLSalarySettingRows(bcDocument.getDocumentNumber(), ssObjects);

        return pbglSalarySettingRows;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#getDetailSalarySettingLaborObjects(java.lang.Integer, java.lang.String)
     */
    @NonTransactional
    public List<String> getDetailSalarySettingLaborObjects(Integer universityFiscalYear, String chartOfAccountsCode) {
        List<String> detailSalarySettingObjects = new ArrayList<String>();

        Map<String, Object> laborObjectCodeMap = new HashMap<String, Object>();
        laborObjectCodeMap.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, universityFiscalYear);
        laborObjectCodeMap.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, chartOfAccountsCode);
        laborObjectCodeMap.put(KFSPropertyConstants.DETAIL_POSITION_REQUIRED_INDICATOR, true);
        List<LaborLedgerObject> laborLedgerObjects = kualiModuleService.getResponsibleModuleService(LaborLedgerObject.class).getExternalizableBusinessObjectsList(LaborLedgerObject.class, laborObjectCodeMap);

        for (LaborLedgerObject laborObject : laborLedgerObjects) {
            detailSalarySettingObjects.add(laborObject.getFinancialObjectCode());
        }

        return detailSalarySettingObjects;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#addOrUpdatePBGLRow(org.kuali.kfs.module.bc.document.BudgetConstructionDocument,
     * org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionGeneralLedger)
     */
    @NonTransactional
    public BudgetConstructionDocument addOrUpdatePBGLRow(BudgetConstructionDocument bcDoc, PendingBudgetConstructionGeneralLedger sourceRow, boolean isRevenue) {

        List<PendingBudgetConstructionGeneralLedger> pbglRows;
        if (isRevenue) {
            pbglRows = bcDoc.getPendingBudgetConstructionGeneralLedgerRevenueLines();
        } else {
            pbglRows = bcDoc.getPendingBudgetConstructionGeneralLedgerExpenditureLines();
        }

        // add or update salary setting row to set in memory - this assumes at least one row in the set
        // we can't even do salary setting without at least one salary detail row
        int index = 0;
        boolean insertNeeded = true;
        for (PendingBudgetConstructionGeneralLedger pbglRow : pbglRows) {
            String pbglRowKey = pbglRow.getFinancialObjectCode() + pbglRow.getFinancialSubObjectCode();
            String sourceRowKey = sourceRow.getFinancialObjectCode() + sourceRow.getFinancialSubObjectCode();
            if (pbglRowKey.compareToIgnoreCase(sourceRowKey) == 0) {
                // update
                insertNeeded = false;
                pbglRow.setAccountLineAnnualBalanceAmount(sourceRow.getAccountLineAnnualBalanceAmount());
                pbglRow.setPersistedAccountLineAnnualBalanceAmount(sourceRow.getAccountLineAnnualBalanceAmount());
                pbglRow.setVersionNumber(sourceRow.getVersionNumber());
                break;
            } else {
                if (pbglRowKey.compareToIgnoreCase(sourceRowKey) > 0) {
                    // insert here - drop out
                    break;
                }
            }
            index++;
        }
        if (insertNeeded) {
            // insert the row
            sourceRow.setPersistedAccountLineAnnualBalanceAmount(sourceRow.getAccountLineAnnualBalanceAmount());
            pbglRows.add(index, sourceRow);
        }

        return bcDoc;
    }

    /**
     * Reloads benefits target accounting lines. Usually called right after an annual benefits calculation and the display needs
     * updated with a fresh copy from the database. All old row versions are removed and database row versions are inserted in the
     * list in the correct order.
     *
     * @param bcDoc
     */
    @Transactional
    protected void reloadBenefitsLines(BudgetConstructionDocument bcDoc) {

        // get list of potential fringe objects to use as an in query param
        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, bcDoc.getUniversityFiscalYear());
        fieldValues.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, bcDoc.getChartOfAccountsCode());

        List<LaborLedgerBenefitsCalculation> benefitsCalculation = kualiModuleService.getResponsibleModuleService(LaborLedgerBenefitsCalculation.class).getExternalizableBusinessObjectsList(LaborLedgerBenefitsCalculation.class, fieldValues);

        List<String> fringeObjects = new ArrayList<String>();
        for (LaborLedgerBenefitsCalculation element : benefitsCalculation) {
            fringeObjects.add(element.getPositionFringeBenefitObjectCode());
        }

        List<PendingBudgetConstructionGeneralLedger> dbPBGLFringeLines = budgetConstructionDao.getDocumentPBGLFringeLines(bcDoc.getDocumentNumber(), fringeObjects);
        List<PendingBudgetConstructionGeneralLedger> docPBGLExpLines = bcDoc.getPendingBudgetConstructionGeneralLedgerExpenditureLines();

        // holds the request sums of removed, added records and used to adjust the document expenditure request total
        KualiInteger docRequestTotals = KualiInteger.ZERO;
        KualiInteger dbRequestTotals = KualiInteger.ZERO;

        // remove the current set of fringe lines
        ListIterator docLines = docPBGLExpLines.listIterator();
        while (docLines.hasNext()) {
            PendingBudgetConstructionGeneralLedger docLine = (PendingBudgetConstructionGeneralLedger) docLines.next();
            if (fringeObjects.contains(docLine.getFinancialObjectCode())) {
                docRequestTotals = docRequestTotals.add(docLine.getAccountLineAnnualBalanceAmount());
                docLines.remove();
            }
        }

        // add the dbset of fringe lines, if any
        if (dbPBGLFringeLines != null && !dbPBGLFringeLines.isEmpty()) {

            if (docPBGLExpLines == null || docPBGLExpLines.isEmpty()) {
                docPBGLExpLines.addAll(dbPBGLFringeLines);
            } else {
                ListIterator dbLines = dbPBGLFringeLines.listIterator();
                docLines = docPBGLExpLines.listIterator();
                PendingBudgetConstructionGeneralLedger dbLine = (PendingBudgetConstructionGeneralLedger) dbLines.next();
                PendingBudgetConstructionGeneralLedger docLine = (PendingBudgetConstructionGeneralLedger) docLines.next();
                boolean dbDone = false;
                boolean docDone = false;
                while (!dbDone) {
                    if (docDone || docLine.getFinancialObjectCode().compareToIgnoreCase(dbLine.getFinancialObjectCode()) > 0) {
                        if (!docDone) {
                            docLine = (PendingBudgetConstructionGeneralLedger) docLines.previous();
                        }
                        dbRequestTotals = dbRequestTotals.add(dbLine.getAccountLineAnnualBalanceAmount());
                        dbLine.setPersistedAccountLineAnnualBalanceAmount(dbLine.getAccountLineAnnualBalanceAmount());
                        this.populatePBGLLine(dbLine);
                        docLines.add(dbLine);
                        if (!docDone) {
                            docLine = (PendingBudgetConstructionGeneralLedger) docLines.next();
                        }
                        if (dbLines.hasNext()) {
                            dbLine = (PendingBudgetConstructionGeneralLedger) dbLines.next();
                        } else {
                            dbDone = true;
                        }
                    } else {
                        if (docLines.hasNext()) {
                            docLine = (PendingBudgetConstructionGeneralLedger) docLines.next();
                        } else {
                            docDone = true;
                        }
                    }
                }
            }
        }

        // adjust the request total for the removed and added recs
        bcDoc.setExpenditureAccountLineAnnualBalanceAmountTotal(bcDoc.getExpenditureAccountLineAnnualBalanceAmountTotal().add(dbRequestTotals.subtract(docRequestTotals)));

    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#populatePBGLLine(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionGeneralLedger)
     */
    @Transactional
    public void populatePBGLLine(PendingBudgetConstructionGeneralLedger line) {

        final List REFRESH_FIELDS;
        if (StringUtils.isNotBlank(line.getFinancialSubObjectCode())) {
            REFRESH_FIELDS = Collections.unmodifiableList(Arrays.asList(new String[]{KFSPropertyConstants.FINANCIAL_OBJECT, KFSPropertyConstants.FINANCIAL_SUB_OBJECT, BCPropertyConstants.BUDGET_CONSTRUCTION_MONTHLY}));
        } else {
            REFRESH_FIELDS = Collections.unmodifiableList(Arrays.asList(new String[]{KFSPropertyConstants.FINANCIAL_OBJECT, BCPropertyConstants.BUDGET_CONSTRUCTION_MONTHLY}));
        }
        persistenceService.retrieveReferenceObjects(line, REFRESH_FIELDS);

    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#getPendingBudgetConstructionAppointmentFundingRequestSum(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionGeneralLedger)
     */
    @Transactional
    public KualiInteger getPendingBudgetConstructionAppointmentFundingRequestSum(PendingBudgetConstructionGeneralLedger salaryDetailLine) {
        return budgetConstructionDao.getPendingBudgetConstructionAppointmentFundingRequestSum(salaryDetailLine);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#isBudgetableDocument(org.kuali.kfs.module.bc.businessobject.BudgetConstructionHeader)
     */
    @NonTransactional
    public boolean isBudgetableDocument(BudgetConstructionHeader bcHeader) {
        if (bcHeader == null) {
            return false;
        }

        Integer budgetYear = bcHeader.getUniversityFiscalYear();
        Account account = bcHeader.getAccount();
        boolean isBudgetableAccount = this.isBudgetableAccount(budgetYear, account, true);

        if (isBudgetableAccount) {
            SubAccount subAccount = bcHeader.getSubAccount();
            String subAccountNumber = bcHeader.getSubAccountNumber();

            return this.isBudgetableSubAccount(subAccount, subAccountNumber);
        }

        return false;
    }

    @NonTransactional
    public boolean isBudgetableDocumentNoWagesCheck(BudgetConstructionHeader bcHeader) {
        if (bcHeader == null) {
            return false;
        }

        Integer budgetYear = bcHeader.getUniversityFiscalYear();
        Account account = bcHeader.getAccount();
        boolean isBudgetableAccount = this.isBudgetableAccount(budgetYear, account, false);

        if (isBudgetableAccount) {
            SubAccount subAccount = bcHeader.getSubAccount();
            String subAccountNumber = bcHeader.getSubAccountNumber();

            return this.isBudgetableSubAccount(subAccount, subAccountNumber);
        }

        return false;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#isBudgetableDocument(org.kuali.kfs.module.bc.document.BudgetConstructionDocument)
     */
    @NonTransactional
    public boolean isBudgetableDocument(BudgetConstructionDocument document) {
        if (document == null) {
            return false;
        }

        Integer budgetYear = document.getUniversityFiscalYear();
        Account account = document.getAccount();
        boolean isBudgetableAccount = this.isBudgetableAccount(budgetYear, account, true);

        if (isBudgetableAccount) {
            SubAccount subAccount = document.getSubAccount();
            String subAccountNumber = document.getSubAccountNumber();

            return this.isBudgetableSubAccount(subAccount, subAccountNumber);
        }

        return false;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#isBudgetableDocumentNoWagesCheck(org.kuali.kfs.module.bc.document.BudgetConstructionDocument)
     */
    @NonTransactional
    public boolean isBudgetableDocumentNoWagesCheck(BudgetConstructionDocument document) {
        if (document == null) {
            return false;
        }

        Integer budgetYear = document.getUniversityFiscalYear();
        Account account = document.getAccount();
        boolean isBudgetableAccount = this.isBudgetableAccount(budgetYear, account, false);

        if (isBudgetableAccount) {
            SubAccount subAccount = document.getSubAccount();
            String subAccountNumber = document.getSubAccountNumber();

            return this.isBudgetableSubAccount(subAccount, subAccountNumber);
        }

        return false;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#isAssociatedWithBudgetableDocument(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    @NonTransactional
    public boolean isAssociatedWithBudgetableDocument(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        BudgetConstructionHeader bcHeader = this.getBudgetConstructionHeader(appointmentFunding);
        return this.isBudgetableDocument(bcHeader);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#isBudgetableAccount(java.lang.Integer,
     * org.kuali.kfs.coa.businessobject.Account)
     */
    @NonTransactional
    public boolean isBudgetableAccount(Integer budgetYear, Account account, boolean isWagesCheck) {
        if (budgetYear == null || account == null) {
            return false;
        }

        // account cannot be closed.
        if (!account.isActive()) {
            return false;
        }

        // account cannot be expired before beginning of 6th accounting period, 2 years before budget construction fiscal year.
        Calendar expDate = BudgetConstructionRuleUtil.getNoBudgetAllowedExpireDate(budgetYear);
        if (account.isExpired(expDate)) {
            return false;
        }

        // account cannot be a cash control account
        if (StringUtils.equals(account.getBudgetRecordingLevelCode(), BCConstants.BUDGET_RECORDING_LEVEL_N)) {
            return false;
        }

        // this check is needed for salary setting
        if (isWagesCheck) {

            // account must be flagged as wages allowed
            SubFundGroup subFundGroup = account.getSubFundGroup();
            if (subFundGroup == null || !subFundGroup.isSubFundGroupWagesIndicator()) {
                return false;
            }
        }

        return true;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#isBudgetableSubAccount(org.kuali.kfs.coa.businessobject.SubAccount,
     * java.lang.String)
     */
    @NonTransactional
    public boolean isBudgetableSubAccount(SubAccount subAccount, String subAccountNumber) {
        if (StringUtils.isNotEmpty(subAccountNumber) && StringUtils.equals(subAccountNumber, KFSConstants.getDashSubAccountNumber())) {
            return true;
        }

        // sub account must exist and be active.
        if (ObjectUtils.isNull(subAccount) || !subAccount.isActive()) {
            return false;
        }

        // sub account must not be flagged cost share
        A21SubAccount a21SubAccount = subAccount.getA21SubAccount();
        if (ObjectUtils.isNotNull(a21SubAccount) && StringUtils.equals(a21SubAccount.getSubAccountTypeCode(), KFSConstants.SubAccountType.COST_SHARE)) {
            return false;
        }

        return true;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#isAccountReportsExist(java.lang.String, java.lang.String)
     */
    @Transactional
    public boolean isAccountReportsExist(String chartOfAccountsCode, String accountNumber) {

        BudgetConstructionAccountReports accountReports = (BudgetConstructionAccountReports) budgetConstructionDao.getAccountReports(chartOfAccountsCode, accountNumber);
        if (accountReports == null) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#updatePendingBudgetGeneralLedger(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding,
     * org.kuali.rice.core.api.util.type.KualiInteger)
     */
    @Transactional
    public void updatePendingBudgetGeneralLedger(PendingBudgetConstructionAppointmentFunding appointmentFunding, KualiInteger updateAmount) {
        BudgetConstructionHeader budgetConstructionHeader = this.getBudgetConstructionHeader(appointmentFunding);
        if (budgetConstructionHeader == null) {
            return;
        }

        PendingBudgetConstructionGeneralLedger pendingRecord = this.getPendingBudgetConstructionGeneralLedger(budgetConstructionHeader, appointmentFunding, updateAmount, false);
        businessObjectService.save(pendingRecord);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#updatePendingBudgetGeneralLedgerPlug(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding,
     * org.kuali.rice.core.api.util.type.KualiInteger)
     */
    @Transactional
    public void updatePendingBudgetGeneralLedgerPlug(PendingBudgetConstructionAppointmentFunding appointmentFunding, KualiInteger updateAmount) {
        if (updateAmount == null) {
            throw new IllegalArgumentException("The update amount cannot be null");
        }

        BudgetConstructionHeader budgetConstructionHeader = this.getBudgetConstructionHeader(appointmentFunding);
        if (budgetConstructionHeader == null) {
            return;
        }

        if (this.canUpdatePlugRecord(appointmentFunding)) {
            PendingBudgetConstructionGeneralLedger plugRecord = this.getPendingBudgetConstructionGeneralLedger(budgetConstructionHeader, appointmentFunding, updateAmount, true);

            KualiInteger annualBalanceAmount = plugRecord.getAccountLineAnnualBalanceAmount();
            KualiInteger beginningBalanceAmount = plugRecord.getFinancialBeginningBalanceLineAmount();

            if ((annualBalanceAmount == null || annualBalanceAmount.isZero()) && (beginningBalanceAmount == null || beginningBalanceAmount.isZero())) {
                businessObjectService.delete(plugRecord);
            } else {
                businessObjectService.save(plugRecord);
            }
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#updatePendingBudgetGeneralLedgerPlug(org.kuali.kfs.module.bc.document.BudgetConstructionDocument,
     * org.kuali.rice.core.api.util.type.KualiInteger)
     */
    @Transactional
    public PendingBudgetConstructionGeneralLedger updatePendingBudgetGeneralLedgerPlug(BudgetConstructionDocument bcDoc, KualiInteger updateAmount) {

        String twoPlugKey = KFSConstants.BudgetConstructionConstants.OBJECT_CODE_2PLG + KFSConstants.getDashFinancialSubObjectCode();
        List<PendingBudgetConstructionGeneralLedger> expenditureRows = bcDoc.getPendingBudgetConstructionGeneralLedgerExpenditureLines();
        PendingBudgetConstructionGeneralLedger twoPlugRow = null;

        // update or insert the 2plg row - this assumes at least one row in the set
        // we can't even do salary setting without at least one detail line
        int index = 0;
        boolean insertNeeded = true;
        for (PendingBudgetConstructionGeneralLedger expRow : expenditureRows) {
            String expRowKey = expRow.getFinancialObjectCode() + expRow.getFinancialSubObjectCode();
            if (expRowKey.compareToIgnoreCase(twoPlugKey) == 0) {

                // update the existing row
                insertNeeded = false;
                expRow.setAccountLineAnnualBalanceAmount(expRow.getAccountLineAnnualBalanceAmount().add(updateAmount.negated()));
                expRow.setPersistedAccountLineAnnualBalanceAmount(expRow.getAccountLineAnnualBalanceAmount());
                businessObjectService.save(expRow);
                expRow.refresh();
                twoPlugRow = expRow;
                break;
            } else {
                if (expRowKey.compareToIgnoreCase(twoPlugKey) > 0) {

                    // case where offsetting salary setting updates under different object codes - insert a new row here
                    break;
                }
            }
            index++;
        }
        if (insertNeeded) {

            // do insert in the middle or at end of list
            String objectCode = KFSConstants.BudgetConstructionConstants.OBJECT_CODE_2PLG;
            String subObjectCode = KFSConstants.getDashFinancialSubObjectCode();
            String objectTypeCode = optionsService.getOptions(bcDoc.getUniversityFiscalYear()).getFinObjTypeExpenditureexpCd();

            PendingBudgetConstructionGeneralLedger pendingRecord = new PendingBudgetConstructionGeneralLedger();

            pendingRecord.setDocumentNumber(bcDoc.getDocumentNumber());
            pendingRecord.setUniversityFiscalYear(bcDoc.getUniversityFiscalYear());
            pendingRecord.setChartOfAccountsCode(bcDoc.getChartOfAccountsCode());
            pendingRecord.setAccountNumber(bcDoc.getAccountNumber());
            pendingRecord.setSubAccountNumber(bcDoc.getSubAccountNumber());

            pendingRecord.setFinancialObjectCode(objectCode);
            pendingRecord.setFinancialSubObjectCode(subObjectCode);
            pendingRecord.setFinancialBalanceTypeCode(KFSConstants.BALANCE_TYPE_BASE_BUDGET);
            pendingRecord.setFinancialObjectTypeCode(objectTypeCode);

            pendingRecord.setFinancialBeginningBalanceLineAmount(KualiInteger.ZERO);
            pendingRecord.setAccountLineAnnualBalanceAmount(updateAmount);

            // store and add to memory set
            pendingRecord.setPersistedAccountLineAnnualBalanceAmount(pendingRecord.getAccountLineAnnualBalanceAmount());
            businessObjectService.save(pendingRecord);
            expenditureRows.add(index, pendingRecord);
            twoPlugRow = pendingRecord;
            bcDoc.setContainsTwoPlug(true);
        }

        bcDoc.setExpenditureAccountLineAnnualBalanceAmountTotal(bcDoc.getExpenditureAccountLineAnnualBalanceAmountTotal().add(updateAmount.negated()));
        return twoPlugRow;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#getBudgetConstructionHeader(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    @NonTransactional
    public BudgetConstructionHeader getBudgetConstructionHeader(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        String chartOfAccountsCode = appointmentFunding.getChartOfAccountsCode();
        String accountNumber = appointmentFunding.getAccountNumber();
        String subAccountNumber = appointmentFunding.getSubAccountNumber();
        Integer fiscalYear = appointmentFunding.getUniversityFiscalYear();

        return this.getByCandidateKey(chartOfAccountsCode, accountNumber, subAccountNumber, fiscalYear);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#getBudgetConstructionDocument(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    @NonTransactional
    public BudgetConstructionDocument getBudgetConstructionDocument(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, appointmentFunding.getUniversityFiscalYear());
        fieldValues.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, appointmentFunding.getChartOfAccountsCode());
        fieldValues.put(KFSPropertyConstants.ACCOUNT_NUMBER, appointmentFunding.getAccountNumber());
        fieldValues.put(KFSPropertyConstants.SUB_ACCOUNT_NUMBER, appointmentFunding.getSubAccountNumber());

        // fiscalyear, chart, account, subaccount is a candidate key for BC document
        // This should not need the special handling and just return the first (only document) in the collection
        Collection<BudgetConstructionDocument> documents = businessObjectService.findMatching(BudgetConstructionDocument.class, fieldValues);
        for (BudgetConstructionDocument document : documents) {
            try {
                return (BudgetConstructionDocument) documentService.getByDocumentHeaderId(document.getDocumentHeader().getDocumentNumber());
            } catch (WorkflowException e) {
                throw new RuntimeException("Fail to retrieve the document for appointment funding" + appointmentFunding, e);
            }
        }

        return null;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#getBudgetConstructionDocument(org.kuali.kfs.module.bc.businessobject.SalarySettingExpansion)
     */
    @NonTransactional
    public BudgetConstructionDocument getBudgetConstructionDocument(SalarySettingExpansion salarySettingExpansion) {
        try {
            return (BudgetConstructionDocument) documentService.getByDocumentHeaderId(salarySettingExpansion.getDocumentNumber());
        } catch (WorkflowException e) {
            throw new RuntimeException("Fail to retrieve the document for salary expansion" + salarySettingExpansion, e);
        }
    }

    /**
     * determine whether the plug line can be updated or created. If the given appointment funding is in the plug override mode or
     * it associates with a contract and grant account, then no plug can be updated or created
     *
     * @param appointmentFunding the given appointment funding
     * @return true if the plug line can be updated or created; otherwise, false
     */
    @Transactional
    protected boolean canUpdatePlugRecord(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        // no plug if the override mode is enabled
        if (appointmentFunding.isOverride2PlugMode()) {
            return false;
        }

        Account account = appointmentFunding.getAccount();

        // no plug for the account with the sub groups setup as a system parameter
        if (BudgetParameterFinder.getNotGenerate2PlgSubFundGroupCodes().contains(account.getSubFundGroupCode())) {
            return false;
        }

        // no plug for the contract and grant account
        if (account.isForContractsAndGrants()) {
            return false;
        }

        return true;
    }


    /**
     * get a pending budget construction GL record, and set its to the given update amount if it exists in database; otherwise,
     * create it with the given information
     *
     * @param budgetConstructionHeader the budget construction header of the pending budget construction GL record
     * @param appointmentFunding       the appointment funding associated with the pending budget construction GL record
     * @param updateAmount             the amount being used to update the retrieved pending budget construction GL record
     * @param is2PLG                   the flag used to instrcut to retrieve a pending budget construction GL plug record
     * @return a pending budget construction GL record if any; otherwise, create one with the given information
     */
    @Transactional
    protected PendingBudgetConstructionGeneralLedger getPendingBudgetConstructionGeneralLedger(BudgetConstructionHeader budgetConstructionHeader, PendingBudgetConstructionAppointmentFunding appointmentFunding, KualiInteger updateAmount, boolean is2PLG) {
        if (budgetConstructionHeader == null) {
            throw new IllegalArgumentException("The given budget construction document header cannot be null");
        }

        if (appointmentFunding == null) {
            throw new IllegalArgumentException("The given pending budget appointment funding cannot be null");
        }

        if (updateAmount == null) {
            throw new IllegalArgumentException("The update amount cannot be null");
        }

        PendingBudgetConstructionGeneralLedger pendingRecord = this.retrievePendingBudgetConstructionGeneralLedger(budgetConstructionHeader, appointmentFunding, is2PLG);

        if (pendingRecord != null) {
            KualiInteger newAnnaulBalanceAmount = pendingRecord.getAccountLineAnnualBalanceAmount().add(updateAmount);
            pendingRecord.setAccountLineAnnualBalanceAmount(newAnnaulBalanceAmount);
        } else if (!is2PLG || (is2PLG && updateAmount.isNonZero())) {
            // initialize a new pending record if not plug line or plug line not zero

            Integer budgetYear = appointmentFunding.getUniversityFiscalYear();
            String objectCode = is2PLG ? KFSConstants.BudgetConstructionConstants.OBJECT_CODE_2PLG : appointmentFunding.getFinancialObjectCode();
            String subObjectCode = is2PLG ? KFSConstants.getDashFinancialSubObjectCode() : appointmentFunding.getFinancialSubObjectCode();
            String objectTypeCode = optionsService.getOptions(budgetYear).getFinObjTypeExpenditureexpCd();

            pendingRecord = new PendingBudgetConstructionGeneralLedger();

            pendingRecord.setDocumentNumber(budgetConstructionHeader.getDocumentNumber());
            pendingRecord.setUniversityFiscalYear(appointmentFunding.getUniversityFiscalYear());
            pendingRecord.setChartOfAccountsCode(appointmentFunding.getChartOfAccountsCode());
            pendingRecord.setAccountNumber(appointmentFunding.getAccountNumber());
            pendingRecord.setSubAccountNumber(appointmentFunding.getSubAccountNumber());

            pendingRecord.setFinancialObjectCode(objectCode);
            pendingRecord.setFinancialSubObjectCode(subObjectCode);
            pendingRecord.setFinancialBalanceTypeCode(KFSConstants.BALANCE_TYPE_BASE_BUDGET);
            pendingRecord.setFinancialObjectTypeCode(objectTypeCode);

            pendingRecord.setFinancialBeginningBalanceLineAmount(KualiInteger.ZERO);
            pendingRecord.setAccountLineAnnualBalanceAmount(updateAmount);
        }

        return pendingRecord;
    }

    /**
     * retrieve a pending budget construction GL record based on the given infromation
     *
     * @param budgetConstructionHeader the budget construction header of the pending budget construction GL record to be retrieved
     * @param appointmentFunding       the appointment funding associated with the pending budget construction GL record to be retrieved
     * @param is2PLG                   the flag used to instrcut to retrieve a pending budget construction GL plug record
     * @return a pending budget construction GL record if any; otherwise, null
     */
    @NonTransactional
    protected PendingBudgetConstructionGeneralLedger retrievePendingBudgetConstructionGeneralLedger(BudgetConstructionHeader budgetConstructionHeader, PendingBudgetConstructionAppointmentFunding appointmentFunding, boolean is2PLG) {
        String objectCode = is2PLG ? KFSConstants.BudgetConstructionConstants.OBJECT_CODE_2PLG : appointmentFunding.getFinancialObjectCode();
        String subObjectCode = is2PLG ? KFSConstants.getDashFinancialSubObjectCode() : appointmentFunding.getFinancialSubObjectCode();

        Map<String, Object> searchCriteria = new HashMap<String, Object>();

        searchCriteria.put(KFSPropertyConstants.DOCUMENT_NUMBER, budgetConstructionHeader.getDocumentNumber());
        searchCriteria.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, budgetConstructionHeader.getUniversityFiscalYear());
        searchCriteria.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, budgetConstructionHeader.getChartOfAccountsCode());
        searchCriteria.put(KFSPropertyConstants.ACCOUNT_NUMBER, budgetConstructionHeader.getAccountNumber());
        searchCriteria.put(KFSPropertyConstants.SUB_ACCOUNT_NUMBER, budgetConstructionHeader.getSubAccountNumber());
        searchCriteria.put(KFSPropertyConstants.FINANCIAL_BALANCE_TYPE_CODE, KFSConstants.BALANCE_TYPE_BASE_BUDGET);
        searchCriteria.put(KFSPropertyConstants.FINANCIAL_OBJECT_TYPE_CODE, optionsService.getOptions(appointmentFunding.getUniversityFiscalYear()).getFinObjTypeExpenditureexpCd());

        searchCriteria.put(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, objectCode);
        searchCriteria.put(KFSPropertyConstants.FINANCIAL_SUB_OBJECT_CODE, subObjectCode);

        return (PendingBudgetConstructionGeneralLedger) businessObjectService.findByPrimaryKey(PendingBudgetConstructionGeneralLedger.class, searchCriteria);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#retrievePendingBudgetConstructionGeneralLedger(org.kuali.kfs.module.bc.businessobject.BudgetConstructionHeader)
     */
    @NonTransactional
    public List<PendingBudgetConstructionGeneralLedger> retrievePendingBudgetConstructionGeneralLedger(BudgetConstructionHeader budgetConstructionHeader) {
        Map<String, Object> searchCriteria = new HashMap<String, Object>();

        searchCriteria.put(KFSPropertyConstants.DOCUMENT_NUMBER, budgetConstructionHeader.getDocumentNumber());
        searchCriteria.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, budgetConstructionHeader.getUniversityFiscalYear());
        searchCriteria.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, budgetConstructionHeader.getChartOfAccountsCode());
        searchCriteria.put(KFSPropertyConstants.ACCOUNT_NUMBER, budgetConstructionHeader.getAccountNumber());
        searchCriteria.put(KFSPropertyConstants.SUB_ACCOUNT_NUMBER, budgetConstructionHeader.getSubAccountNumber());

        return (List<PendingBudgetConstructionGeneralLedger>) businessObjectService.findMatching(PendingBudgetConstructionGeneralLedger.class, searchCriteria);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#retrieveOrBuildAccountOrganizationHierarchy(java.lang.Integer,
     * java.lang.String, java.lang.String)
     */
    @Transactional
    public List<BudgetConstructionAccountOrganizationHierarchy> retrieveOrBuildAccountOrganizationHierarchy(Integer universityFiscalYear, String chartOfAccountsCode, String accountNumber) {

        List<BudgetConstructionAccountOrganizationHierarchy> accountOrgHier = new ArrayList<BudgetConstructionAccountOrganizationHierarchy>();
        BudgetConstructionAccountReports accountReports = (BudgetConstructionAccountReports) budgetConstructionDao.getAccountReports(chartOfAccountsCode, accountNumber);
        if (accountReports != null) {
            accountOrgHier = budgetConstructionDao.getAccountOrgHierForAccount(chartOfAccountsCode, accountNumber, universityFiscalYear);
            if (accountOrgHier == null || accountOrgHier.isEmpty()) {

                // attempt to build it
                String[] rootNode = organizationService.getRootOrganizationCode();
                String rootChart = rootNode[0];
                String rootOrganization = rootNode[1];
                Integer currentLevel = new Integer(1);
                String organizationChartOfAccountsCode = accountReports.getReportsToChartOfAccountsCode();
                String organizationCode = accountReports.getReportsToOrganizationCode();
                boolean overFlow = budgetConstructionDao.insertAccountIntoAccountOrganizationHierarchy(rootChart, rootOrganization, universityFiscalYear, chartOfAccountsCode, accountNumber, currentLevel, organizationChartOfAccountsCode, organizationCode);
                if (!overFlow) {
                    accountOrgHier = budgetConstructionDao.getAccountOrgHierForAccount(chartOfAccountsCode, accountNumber, universityFiscalYear);
                }
            }
        }
        return accountOrgHier;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#instantiateNewBudgetConstructionDocument(org.kuali.kfs.module.bc.document.BudgetConstructionDocument)
     */
    @Transactional
    public BudgetConstructionDocument instantiateNewBudgetConstructionDocument(BudgetConstructionDocument budgetConstructionDocument) throws WorkflowException {

        budgetConstructionDocument.setOrganizationLevelChartOfAccountsCode(BCConstants.INITIAL_ORGANIZATION_LEVEL_CHART_OF_ACCOUNTS_CODE);
        budgetConstructionDocument.setOrganizationLevelOrganizationCode(BCConstants.INITIAL_ORGANIZATION_LEVEL_ORGANIZATION_CODE);
        budgetConstructionDocument.setOrganizationLevelCode(BCConstants.INITIAL_ORGANIZATION_LEVEL_CODE);
        budgetConstructionDocument.setBudgetTransactionLockUserIdentifier(BCConstants.DEFAULT_BUDGET_HEADER_LOCK_IDS);
        budgetConstructionDocument.setBudgetLockUserIdentifier(BCConstants.DEFAULT_BUDGET_HEADER_LOCK_IDS);

        FinancialSystemDocumentHeader kualiDocumentHeader = budgetConstructionDocument.getFinancialSystemDocumentHeader();
        budgetConstructionDocument.setDocumentNumber(budgetConstructionDocument.getDocumentHeader().getDocumentNumber());
        kualiDocumentHeader.setOrganizationDocumentNumber(budgetConstructionDocument.getUniversityFiscalYear().toString());
        kualiDocumentHeader.setFinancialDocumentStatusCode(KFSConstants.INITIAL_KUALI_DOCUMENT_STATUS_CD);
        kualiDocumentHeader.setFinancialDocumentTotalAmount(KualiDecimal.ZERO);
        kualiDocumentHeader.setDocumentDescription(String.format("%s %d %s %s", BCConstants.BUDGET_CONSTRUCTION_DOCUMENT_DESCRIPTION, budgetConstructionDocument.getUniversityFiscalYear(), budgetConstructionDocument.getChartOfAccountsCode(), budgetConstructionDocument.getAccountNumber()));
        kualiDocumentHeader.setExplanation(BCConstants.BUDGET_CONSTRUCTION_DOCUMENT_DESCRIPTION);
        List<AdHocRouteRecipient> emptyAdHocList = new ArrayList<AdHocRouteRecipient>();

        // call route with document type configured for no route paths or post processor
        documentService.routeDocument(budgetConstructionDocument, "created by application UI", emptyAdHocList);

        return budgetConstructionDocument;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.BudgetDocumentService#getPushPullLevelList(org.kuali.kfs.module.bc.document.BudgetConstructionDocument,
     * org.kuali.rice.kim.api.identity.Person)
     */
    @Transactional
    public List<BudgetConstructionAccountOrganizationHierarchy> getPushPullLevelList(BudgetConstructionDocument bcDoc, Person person) {
        List<BudgetConstructionAccountOrganizationHierarchy> pushOrPullList = new ArrayList<BudgetConstructionAccountOrganizationHierarchy>();

        pushOrPullList.addAll(budgetConstructionDao.getAccountOrgHierForAccount(bcDoc.getChartOfAccountsCode(), bcDoc.getAccountNumber(), bcDoc.getUniversityFiscalYear()));

        if (pushOrPullList.size() >= 1) {
            BudgetConstructionAccountOrganizationHierarchy levelZero = new BudgetConstructionAccountOrganizationHierarchy();
            levelZero.setUniversityFiscalYear(bcDoc.getUniversityFiscalYear());
            levelZero.setChartOfAccountsCode(bcDoc.getChartOfAccountsCode());
            levelZero.setAccountNumber(bcDoc.getAccountNumber());
            levelZero.setOrganizationLevelCode(0);
            levelZero.setOrganizationChartOfAccountsCode(pushOrPullList.get(0).getOrganizationChartOfAccountsCode());
            levelZero.setOrganizationCode(pushOrPullList.get(0).getOrganizationCode());
            pushOrPullList.add(0, levelZero);
        }

        return pushOrPullList;
    }

    /**
     * Sets the budgetConstructionDao attribute value.
     *
     * @param budgetConstructionDao The budgetConstructionDao to set.
     */
    @NonTransactional
    public void setBudgetConstructionDao(BudgetConstructionDao budgetConstructionDao) {
        this.budgetConstructionDao = budgetConstructionDao;
    }

    /**
     * Sets the documentService attribute value.
     *
     * @param documentService The documentService to set.
     */
    @NonTransactional
    public void setDocumentService(DocumentService documentService) {
        this.documentService = documentService;
    }

    /**
     * Sets the workflowDocumentService attribute value.
     *
     * @param workflowDocumentService The workflowDocumentService to set.
     */
    @NonTransactional
    public void setWorkflowDocumentService(WorkflowDocumentService workflowDocumentService) {
        this.workflowDocumentService = workflowDocumentService;
    }

    /**
     * Sets the documentDao attribute value.
     *
     * @param documentDao The documentDao to set.
     */
    @NonTransactional
    public void setDocumentDao(DocumentDao documentDao) {
        this.documentDao = documentDao;
    }


    /**
     * Sets the benefitsCalculationService attribute value.
     *
     * @param benefitsCalculationService The benefitsCalculationService to set.
     */
    @NonTransactional
    public void setBenefitsCalculationService(BenefitsCalculationService benefitsCalculationService) {
        this.benefitsCalculationService = benefitsCalculationService;
    }


    /**
     * Sets the businessObjectService attribute value.
     *
     * @param businessObjectService The businessObjectService to set.
     */
    @NonTransactional
    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    /**
     * Sets the budgetParameterService attribute value.
     *
     * @param budgetParameterService The budgetParameterService to set.
     */
    @NonTransactional
    public void setBudgetParameterService(BudgetParameterService budgetParameterService) {
        this.budgetParameterService = budgetParameterService;
    }

    /**
     * Sets the parameterService attribute value.
     *
     * @param parameterService The parameterService to set.
     */
    @NonTransactional
    public void setParameterService(ParameterService parameterService) {
        this.parameterService = parameterService;
    }

    /**
     * Sets the fiscalYearFunctionControlService attribute value.
     *
     * @param fiscalYearFunctionControlService The fiscalYearFunctionControlService to set.
     */
    @NonTransactional
    public void setFiscalYearFunctionControlService(FiscalYearFunctionControlService fiscalYearFunctionControlService) {
        this.fiscalYearFunctionControlService = fiscalYearFunctionControlService;
    }


    /**
     * Sets the optionsService attribute value.
     *
     * @param optionsService The optionsService to set.
     */
    @NonTransactional
    public void setOptionsService(OptionsService optionsService) {
        this.optionsService = optionsService;
    }


    /**
     * Gets the persistenceService attribute.
     *
     * @return Returns the persistenceService.
     */
    @NonTransactional
    public PersistenceService getPersistenceService() {
        return persistenceService;
    }

    /**
     * Sets the persistenceService attribute value.
     *
     * @param persistenceService The persistenceService to set.
     */
    @NonTransactional
    public void setPersistenceService(PersistenceService persistenceService) {
        this.persistenceService = persistenceService;
    }

    /**
     * Sets the organizationService attribute value.
     *
     * @param organizationService The organizationService to set.
     */
    @NonTransactional
    public void setOrganizationService(OrganizationService organizationService) {
        this.organizationService = organizationService;
    }

    /**
     * Sets the kualiModuleService attribute value.
     *
     * @param kualiModuleService The kualiModuleService to set.
     */
    @NonTransactional
    public void setKualiModuleService(KualiModuleService kualiModuleService) {
        this.kualiModuleService = kualiModuleService;
    }

    /**
     * Gets the defaultLaborBenefitRateCategoryCode attribute.
     *
     * @return Returns the defaultLaborBenefitRateCategoryCode.
     */
    @Transactional
    public String getDefaultLaborBenefitRateCategoryCode() {
        if (ObjectUtils.isNull(defaultLaborBenefitRateCategoryCode)) {
            // make sure the parameter exists
            if (SpringContext.getBean(ParameterService.class).parameterExists(Account.class, KFSParameterKeyConstants.LdParameterConstants.DEFAULT_BENEFIT_RATE_CATEGORY_CODE)) {
                this.defaultLaborBenefitRateCategoryCode = SpringContext.getBean(ParameterService.class).getParameterValueAsString(Account.class, KFSParameterKeyConstants.LdParameterConstants.DEFAULT_BENEFIT_RATE_CATEGORY_CODE);
            } else {
                this.defaultLaborBenefitRateCategoryCode = "";
            }
        }
        return defaultLaborBenefitRateCategoryCode;
    }

    /**
     * Sets the defaultLaborBenefitRateCategoryCode attribute value.
     *
     * @param defaultLaborBenefitRateCategoryCode The defaultLaborBenefitRateCategoryCode to set.
     */
    @Transactional
    public void setDefaultLaborBenefitRateCategoryCode(String defaultLaborBenefitRateCategoryCode) {
        this.defaultLaborBenefitRateCategoryCode = defaultLaborBenefitRateCategoryCode;
    }

}
