/*
 * 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.integration.ld.LaborLedgerObject;
import org.kuali.kfs.integration.ld.LaborModuleService;
import org.kuali.kfs.kns.document.authorization.TransactionalDocumentAuthorizer;
import org.kuali.kfs.kns.service.DocumentHelperService;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.service.DocumentService;
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.businessobject.BudgetConstructionAppointmentFundingReason;
import org.kuali.kfs.module.bc.businessobject.BudgetConstructionAppointmentFundingReasonCode;
import org.kuali.kfs.module.bc.businessobject.BudgetConstructionCalculatedSalaryFoundationTracker;
import org.kuali.kfs.module.bc.businessobject.BudgetConstructionHeader;
import org.kuali.kfs.module.bc.businessobject.BudgetConstructionPosition;
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.service.BenefitsCalculationService;
import org.kuali.kfs.module.bc.document.service.BudgetConstructionProcessorService;
import org.kuali.kfs.module.bc.document.service.BudgetDocumentService;
import org.kuali.kfs.module.bc.document.service.LockService;
import org.kuali.kfs.module.bc.document.service.SalarySettingService;
import org.kuali.kfs.module.bc.util.BudgetParameterFinder;
import org.kuali.kfs.module.bc.util.SalarySettingCalculator;
import org.kuali.kfs.module.bc.util.SalarySettingFieldsHolder;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.ObjectUtil;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.service.OptionsService;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.core.api.util.type.KualiInteger;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kim.api.identity.Person;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.kuali.kfs.module.bc.BCConstants.AppointmentFundingDurationCodes.NONE;

/**
 * implements the service methods defined in the SalarySettingService
 *
 * @see org.kuali.kfs.module.bc.document.service.SalarySettingService
 */
@Transactional
public class SalarySettingServiceImpl implements SalarySettingService {
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SalarySettingServiceImpl.class);

    protected ConfigurationService kualiConfigurationService;
    protected BusinessObjectService businessObjectService;
    protected LaborModuleService laborModuleService;
    protected BudgetDocumentService budgetDocumentService;
    protected BenefitsCalculationService benefitsCalculationService;
    protected OptionsService optionsService;
    protected LockService lockService;
    protected DocumentHelperService documentHelperService;
    protected DocumentService documentService;
    protected BudgetConstructionProcessorService budgetConstructionProcessorService;

    /**
     * for now just return false, implement application parameter if decision is made implement this functionality
     *
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#isSalarySettingDisabled()
     */
    public boolean isSalarySettingDisabled() {
        return false;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#calculateHourlyPayRate(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public BigDecimal calculateHourlyPayRate(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("calculateHourlyPayRate() start");

        KualiInteger requestedAmount = appointmentFunding.getAppointmentRequestedAmount();
        BigDecimal fteQuantity = this.calculateFteQuantityFromAppointmentFunding(appointmentFunding);

        BigDecimal annualWorkingHours = BigDecimal.valueOf(BudgetParameterFinder.getAnnualWorkingHours());
        BigDecimal totalPayHoursForYear = fteQuantity.multiply(annualWorkingHours);
        BigDecimal hourlyPayRate = BigDecimal.ZERO;
        if (totalPayHoursForYear.compareTo(BigDecimal.ZERO) != 0) {
            hourlyPayRate = requestedAmount.divide(totalPayHoursForYear).setScale(2, BigDecimal.ROUND_HALF_UP);
        }

        return hourlyPayRate;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#calculateAnnualPayAmount(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public KualiInteger calculateAnnualPayAmount(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("calculateAnnualPayAmount() start");

        KualiInteger annualPayAmount = KualiInteger.ZERO;
        BigDecimal hourlyPayRate = appointmentFunding.getAppointmentRequestedPayRate();
        BigDecimal fteQuantity = this.calculateFteQuantityFromAppointmentFunding(appointmentFunding);
        if (fteQuantity.compareTo(BigDecimal.ZERO) == 0) {
            // revert to entered amount for zero FTE to allow error message
            annualPayAmount = appointmentFunding.getAppointmentRequestedAmount();
        } else {
            BigDecimal annualWorkingHours = BigDecimal.valueOf(BudgetParameterFinder.getAnnualWorkingHours());
            BigDecimal totalPayHoursForYear = fteQuantity.multiply(annualWorkingHours);
            annualPayAmount = new KualiInteger(hourlyPayRate.multiply(totalPayHoursForYear));
        }

        return annualPayAmount;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#normalizePayRateAndAmount(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public void normalizePayRateAndAmount(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("normalizePayRateAndAmount() start");

        BigDecimal currentHourlyPayRate = appointmentFunding.getAppointmentRequestedPayRate();
        if (currentHourlyPayRate != null && !currentHourlyPayRate.equals(BigDecimal.ZERO)) {
            KualiInteger annualPayAmount = this.calculateAnnualPayAmount(appointmentFunding);
            appointmentFunding.setAppointmentRequestedAmount(annualPayAmount);
        } else {

            KualiInteger currentAnnualPayAmount = appointmentFunding.getAppointmentRequestedAmount();
            if (currentAnnualPayAmount != null && currentAnnualPayAmount.isNonZero()) {
                BigDecimal hourlyPayRate = this.calculateHourlyPayRate(appointmentFunding);
                appointmentFunding.setAppointmentRequestedPayRate(hourlyPayRate);
            }

            currentHourlyPayRate = appointmentFunding.getAppointmentRequestedPayRate();
            if (currentHourlyPayRate != null) {
                KualiInteger annualPayAmount = this.calculateAnnualPayAmount(appointmentFunding);
                appointmentFunding.setAppointmentRequestedAmount(annualPayAmount);
            } else {
                appointmentFunding.setAppointmentRequestedPayRate(BigDecimal.ZERO);
            }
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#calculateFteQuantityForAppointmentFunding(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public BigDecimal calculateFteQuantityFromAppointmentFunding(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("calculateFteQuantity() start");

        // appointmentFunding.refreshReferenceObject(BCPropertyConstants.BUDGET_CONSTRUCTION_POSITION);
        BudgetConstructionPosition position = appointmentFunding.getBudgetConstructionPosition();
        if (ObjectUtils.isNull(position)) {
            return BigDecimal.ZERO;
        }

        Integer payMonth = position.getIuPayMonths();
        Integer fundingMonth = appointmentFunding.getAppointmentFundingMonth();
        BigDecimal requestedTimePercent = appointmentFunding.getAppointmentRequestedTimePercent();

        return this.calculateFteQuantity(payMonth, fundingMonth, requestedTimePercent);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#calculateFteQuantity(java.lang.Integer, java.lang.Integer,
     * java.math.BigDecimal)
     */
    public BigDecimal calculateFteQuantity(Integer payMonth, Integer fundingMonth, BigDecimal requestedTimePercent) {
        LOG.debug("calculateFteQuantity() start");

        if (payMonth == null || fundingMonth == null || requestedTimePercent == null) {
            return BigDecimal.ZERO;
        }

        BigDecimal payMonthAsDecimal = BigDecimal.valueOf(payMonth);
        BigDecimal fundingMonthAsDecimal = BigDecimal.valueOf(fundingMonth);
        BigDecimal fundingMonthPercent = fundingMonthAsDecimal.divide(payMonthAsDecimal, 5, BigDecimal.ROUND_HALF_UP);

        BigDecimal fteQuantity = requestedTimePercent.multiply(fundingMonthPercent).divide(KFSConstants.ONE_HUNDRED.bigDecimalValue());

        return fteQuantity.setScale(5, BigDecimal.ROUND_HALF_UP);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#calculateCSFFteQuantityFromAppointmentFunding(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public BigDecimal calculateCSFFteQuantityFromAppointmentFunding(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("calculateCSFFteQuantity() start");

        // appointmentFunding.refreshReferenceObject(BCPropertyConstants.BUDGET_CONSTRUCTION_POSITION);
        BudgetConstructionPosition position = appointmentFunding.getBudgetConstructionPosition();
        if (position == null) {
            return BigDecimal.ZERO;
        }

        Integer payMonth = position.getIuPayMonths();
        Integer normalWorkMonth = position.getIuNormalWorkMonths();
        BigDecimal requestedCSFTimePercent = appointmentFunding.getAppointmentRequestedCsfTimePercent();

        return this.calculateCSFFteQuantity(payMonth, normalWorkMonth, requestedCSFTimePercent);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#calculateCSFFteQuantity(java.lang.Integer,
     * java.lang.Integer, java.math.BigDecimal)
     */
    public BigDecimal calculateCSFFteQuantity(Integer payMonth, Integer normalWorkMonth, BigDecimal requestedCSFTimePercent) {
        LOG.debug("calculateCSFFteQuantity() start");

        if (payMonth == null || normalWorkMonth == null || requestedCSFTimePercent == null) {
            return BigDecimal.ZERO;
        }

        BigDecimal payMonthAsDecimal = BigDecimal.valueOf(payMonth);
        BigDecimal normalMonthAsDecimal = BigDecimal.valueOf(normalWorkMonth);
        BigDecimal fundingMonthPercent = normalMonthAsDecimal.divide(payMonthAsDecimal, 5, BigDecimal.ROUND_HALF_UP);

        BigDecimal fteQuantity = requestedCSFTimePercent.multiply(fundingMonthPercent).divide(KFSConstants.ONE_HUNDRED.bigDecimalValue());

        return fteQuantity.setScale(5, BigDecimal.ROUND_HALF_UP);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#isHourlyPaid(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionGeneralLedger)
     */
    public boolean isHourlyPaid(PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger) {
        LOG.debug("isHourlyPaid() start");

        Integer fiscalYear = pendingBudgetConstructionGeneralLedger.getUniversityFiscalYear();
        String chartOfAccountsCode = pendingBudgetConstructionGeneralLedger.getChartOfAccountsCode();
        String objectCode = pendingBudgetConstructionGeneralLedger.getFinancialObjectCode();

        return this.isHourlyPaidObject(fiscalYear, chartOfAccountsCode, objectCode);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#isHourlyPaid(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public boolean isHourlyPaid(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("isHourlyPaid() start");

        Integer fiscalYear = appointmentFunding.getUniversityFiscalYear();
        String chartOfAccountsCode = appointmentFunding.getChartOfAccountsCode();
        String objectCode = appointmentFunding.getFinancialObjectCode();

        return this.isHourlyPaidObject(fiscalYear, chartOfAccountsCode, objectCode);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#isHourlyPaid(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionGeneralLedger)
     */
    public boolean isHourlyPaidObject(Integer fiscalYear, String chartOfAccountsCode, String objectCode) {
        LOG.debug("isHourlyPaid() start");

        LaborLedgerObject laborLedgerObject = laborModuleService.retrieveLaborLedgerObject(fiscalYear, chartOfAccountsCode, objectCode);

        if (laborLedgerObject == null) {
            return false;
        }

        return BudgetParameterFinder.getBiweeklyPayTypeCodes().contains(laborLedgerObject.getFinancialObjectPayTypeCode());
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#canBeVacant(java.util.List,
     * org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public boolean canBeVacant(List<PendingBudgetConstructionAppointmentFunding> appointmentFundings, PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("canBeVacant(List, PendingBudgetConstructionAppointmentFunding) start");

        if (!this.canBeVacant(appointmentFunding)) {
            return false;
        }

        return this.findVacantAppointmentFunding(appointmentFundings, appointmentFunding) == null;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#findVacantAppointmentFunding(java.util.List,
     * org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public PendingBudgetConstructionAppointmentFunding findVacantAppointmentFunding(List<PendingBudgetConstructionAppointmentFunding> appointmentFundings, PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("findVacantAppointmentFunding() start");

        PendingBudgetConstructionAppointmentFunding vacantAppointmentFunding = this.createVacantAppointmentFunding(appointmentFunding);

        return this.findAppointmentFunding(appointmentFundings, vacantAppointmentFunding);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#findAppointmentFunding(java.util.List,
     * org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public PendingBudgetConstructionAppointmentFunding findAppointmentFunding(List<PendingBudgetConstructionAppointmentFunding> appointmentFundings, PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("findAppointmentFunding() start");

        Map<String, Object> keyFieldValues = appointmentFunding.getValuesMap();
        List<String> keyFields = new ArrayList<String>();
        keyFields.addAll(keyFieldValues.keySet());

        // determine whether there is vacant for the given appointment funding in its list
        for (PendingBudgetConstructionAppointmentFunding fundingLine : appointmentFundings) {
            if (ObjectUtil.equals(fundingLine, appointmentFunding, keyFields)) {
                return fundingLine;
            }
        }

        return null;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#canBeVacant(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public boolean canBeVacant(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("canBeVacant() start");

        if (appointmentFunding.isNewLineIndicator()) {
            return false;
        }

        // the given funding line has not been deleted
        if (appointmentFunding.isAppointmentFundingDeleteIndicator()) {
            return false;
        }

        // the given funding line cannot be a vacant line
        String emplid = appointmentFunding.getEmplid();
        if (BCConstants.VACANT_EMPLID.equals(emplid)) {
            return false;
        }

        // check if the associated position is valid and active
        BudgetConstructionPosition position = appointmentFunding.getBudgetConstructionPosition();
        if (position == null || !position.isBudgetedPosition() || !position.isEffective()) {
            return false;
        }

        // check if there is an existing vacant appintment funcding for the given funding line
        boolean hasBeenVacated = this.hasBeenVacated(appointmentFunding);
        if (hasBeenVacated) {
            return false;
        }

        return true;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#vacateAppointmentFunding(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public PendingBudgetConstructionAppointmentFunding vacateAppointmentFunding(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("vacateAppointmentFunding() start");

        PendingBudgetConstructionAppointmentFunding vacantAppointmentFunding = this.createVacantAppointmentFunding(appointmentFunding);
        this.markAsDelete(appointmentFunding);

        return vacantAppointmentFunding;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#vacateAppointmentFunding(java.util.List,
     * org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public PendingBudgetConstructionAppointmentFunding vacateAppointmentFunding(List<PendingBudgetConstructionAppointmentFunding> appointmentFundings, PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        PendingBudgetConstructionAppointmentFunding vacantAppointmentFunding = this.vacateAppointmentFunding(appointmentFunding);

        if (vacantAppointmentFunding != null) {
            appointmentFundings.add(vacantAppointmentFunding);
        }

        return vacantAppointmentFunding;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#purgeAppointmentFundings(java.util.List)
     */
    public void purgeAppointmentFundings(List<PendingBudgetConstructionAppointmentFunding> purgedAppointmentFundings) {
        // remove the purged appointment funding lines and their referenced records
        for (PendingBudgetConstructionAppointmentFunding appointmentFunding : purgedAppointmentFundings) {
            if (!appointmentFunding.isNewLineIndicator()) {
                businessObjectService.delete(appointmentFunding);
            }
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#adjustRequestedSalaryByAmount(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public void adjustRequestedSalaryByAmount(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("adjustRequestedSalaryByAmount() start");

        int inputAdjustmentAmount = appointmentFunding.getAdjustmentAmount().intValue();

        KualiInteger adjustmentAmount = new KualiInteger(inputAdjustmentAmount);
        KualiInteger csfAmount = this.getCsfAmount(appointmentFunding);
        KualiInteger appointmentRequestedAmount = csfAmount.add(adjustmentAmount);

        appointmentFunding.setAppointmentRequestedAmount(appointmentRequestedAmount);

        if (appointmentFunding.isHourlyPaid()) {
            appointmentFunding.setAppointmentRequestedPayRate(BigDecimal.ZERO);
            this.normalizePayRateAndAmount(appointmentFunding);
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#adjustRequestedSalaryByPercent(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public void adjustRequestedSalaryByPercent(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        LOG.debug("adjustRequestedSalaryByPercent() start");

        KualiInteger csfAmount = this.getCsfAmount(appointmentFunding);

        if (csfAmount.isNonZero()) {
            KualiDecimal percent = appointmentFunding.getAdjustmentAmount();
            BigDecimal adjustedAmount = csfAmount.multiply(percent).divide(KFSConstants.ONE_HUNDRED);

            KualiInteger appointmentRequestedAmount = new KualiInteger(adjustedAmount).add(csfAmount);
            appointmentFunding.setAppointmentRequestedAmount(appointmentRequestedAmount);
        }

        if (appointmentFunding.isHourlyPaid()) {
            appointmentFunding.setAppointmentRequestedPayRate(BigDecimal.ZERO);
            this.normalizePayRateAndAmount(appointmentFunding);
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#saveSalarySetting(org.kuali.kfs.module.bc.businessobject.SalarySettingExpansion)
     */
    public void saveSalarySetting(SalarySettingExpansion salarySettingExpansion) {
        LOG.debug("saveSalarySetting() start");

        List<PendingBudgetConstructionAppointmentFunding> appointmentFundings = salarySettingExpansion.getPendingBudgetConstructionAppointmentFunding();
        this.resetDeletedFundingLines(appointmentFundings);
        this.updateAppointmentFundingsBeforeSaving(appointmentFundings);

        KualiInteger requestedAmountTotal = SalarySettingCalculator.getAppointmentRequestedAmountTotal(appointmentFundings);
        KualiInteger changes = KualiInteger.ZERO;

        if (requestedAmountTotal != null) {
            KualiInteger annualBalanceAmount = salarySettingExpansion.getAccountLineAnnualBalanceAmount();
            changes = (annualBalanceAmount != null) ? requestedAmountTotal.subtract(annualBalanceAmount) : requestedAmountTotal;
        }

        salarySettingExpansion.setAccountLineAnnualBalanceAmount(requestedAmountTotal);
        businessObjectService.save(salarySettingExpansion);

        // now create a pseudo funding line if the BCAF list is empty so we can pass it to create 2PLG below
        Boolean wasSalarySettingExpansionBCAFEmpty = salarySettingExpansion.getPendingBudgetConstructionAppointmentFunding().isEmpty();
        if (wasSalarySettingExpansionBCAFEmpty) {
            appointmentFundings.add(this.createPseudoAppointmentFundingLine(salarySettingExpansion));
        }

        // update or create plug line if the total amount has been changed
        if (changes.isNonZero()) {

            budgetDocumentService.updatePendingBudgetGeneralLedgerPlug(appointmentFundings.get(0), changes.negated());
        }
    }

    public void savePBGLSalarySetting(SalarySettingExpansion salarySettingExpansion) {
        LOG.debug("savePBGLSalarySetting() start");

        // gwp - added this method to handle detail salary setting PBGL updates
        // instead of using saveSalarySetting(SalarySettingExpansion salarySettingExpansion)

        List<PendingBudgetConstructionAppointmentFunding> appointmentFundings = salarySettingExpansion.getPendingBudgetConstructionAppointmentFunding();

        // this is already done in saveSalarySetting by a call to saveAppointmentFundings
        // this.resetDeletedFundingLines(appointmentFundings);
        // this.updateAppointmentFundingsBeforeSaving(appointmentFundings);

        KualiInteger requestedAmountTotal = SalarySettingCalculator.getAppointmentRequestedAmountTotal(appointmentFundings);
        KualiInteger changes = KualiInteger.ZERO;

        if (requestedAmountTotal != null) {
            KualiInteger annualBalanceAmount = salarySettingExpansion.getAccountLineAnnualBalanceAmount();
            changes = (annualBalanceAmount != null) ? requestedAmountTotal.subtract(annualBalanceAmount) : requestedAmountTotal;
        }

        // salarySettingExpansion.setAccountLineAnnualBalanceAmount(requestedAmountTotal);
        // businessObjectService.save(salarySettingExpansion);

        // now create a pseudo funding line if the BCAF list is empty so we can pass it to create 2PLG below
        Boolean wasSalarySettingExpansionBCAFEmpty = salarySettingExpansion.getPendingBudgetConstructionAppointmentFunding().isEmpty();
        if (wasSalarySettingExpansionBCAFEmpty) {
            appointmentFundings.add(this.createPseudoAppointmentFundingLine(salarySettingExpansion));
        }

        // For detail salary setting, we need to update existing or create new PBGL row for the BCAF set
        // We can't save salarySettingExpansion here since it will also save the associated BCAF rows
        // which in this case would be rows we might not have worked on.
        // Also, don't save PBGL if it is not in DB already and the BCAF set was empty (no doo doo).
        // that is, save if PBGL exists in DB or BCAF was not empty
        if (salarySettingExpansion.getVersionNumber() != null || !wasSalarySettingExpansionBCAFEmpty) {

            budgetDocumentService.updatePendingBudgetGeneralLedger(appointmentFundings.get(0), changes);
        }

        // update or create plug line if the total amount has been changed
        if (changes.isNonZero()) {

            budgetDocumentService.updatePendingBudgetGeneralLedgerPlug(appointmentFundings.get(0), changes.negated());
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#saveSalarySetting(java.util.List)
     */
    public void saveSalarySetting(List<PendingBudgetConstructionAppointmentFunding> appointmentFundings, Boolean isSalarySettingByIncumbent) {

        // Do the save/delete of BCAF rows from the salary setting detail screen first
        this.saveAppointmentFundings(appointmentFundings);

        // From the DB get the current unique set of SalarySettingExpansions (PBGL)
        // associated with our Incumbent or Position BCAF rows.
        // Each PBGL row from the DB will, in turn, have all the BCAF rows associated, including the
        // ones we just stored as part of this save operation (see above)
        // No one else would be updating these since we have a transaction lock on the account
        Set<SalarySettingExpansion> salarySettingExpansionSet = new HashSet<SalarySettingExpansion>();

        // these keep track of purged/unpurged used to unlock funding
        // when the last line for that account is purged
        Set<SalarySettingExpansion> purgedSseSet = new HashSet<SalarySettingExpansion>();
        Set<SalarySettingExpansion> unpurgedSseSet = new HashSet<SalarySettingExpansion>();

        // these keep track of purged/unpurged used to unlock positions
        // when the last line for that position is purged
        Set<BudgetConstructionPosition> purgedBPOSNSet = new HashSet<BudgetConstructionPosition>();
        Set<BudgetConstructionPosition> unpurgedBPOSNSet = new HashSet<BudgetConstructionPosition>();

        for (PendingBudgetConstructionAppointmentFunding fundingLine : appointmentFundings) {
            SalarySettingExpansion salarySettingExpansion = this.retriveSalarySalarySettingExpansion(fundingLine);

            if (salarySettingExpansion != null) {
                salarySettingExpansionSet.add(salarySettingExpansion);
            } else {
                // No PBGL row yet, create one to work with in memory only for now.
                // Don't set versionNumber, this will indicate this is in memory only,
                // so we can check for the case where there are no BCAF rows in the DB
                // and no PBGL row either. We don't want to create a new zero request PBGL row in this case.
                salarySettingExpansion = new SalarySettingExpansion();
                salarySettingExpansion.setUniversityFiscalYear(fundingLine.getUniversityFiscalYear());
                salarySettingExpansion.setChartOfAccountsCode(fundingLine.getChartOfAccountsCode());
                salarySettingExpansion.setAccountNumber(fundingLine.getAccountNumber());
                salarySettingExpansion.setSubAccountNumber(fundingLine.getSubAccountNumber());
                salarySettingExpansion.setFinancialObjectCode(fundingLine.getFinancialObjectCode());
                salarySettingExpansion.setFinancialSubObjectCode(fundingLine.getFinancialSubObjectCode());
                salarySettingExpansion.setFinancialBalanceTypeCode(optionsService.getOptions(fundingLine.getUniversityFiscalYear()).getBaseBudgetFinancialBalanceTypeCd());
                salarySettingExpansion.setFinancialObjectTypeCode(optionsService.getOptions(fundingLine.getUniversityFiscalYear()).getFinObjTypeExpenditureexpCd());
                salarySettingExpansion.setAccountLineAnnualBalanceAmount(KualiInteger.ZERO);
                salarySettingExpansion.setFinancialBeginningBalanceLineAmount(KualiInteger.ZERO);

                // If this has been created in memory already, the list should already be attached
                // and be in the current salarySettingExpansionSet.
                // This handles the case where at least 2 new BCAF rows for the same non-existent PBGL row
                // were saved earlier as part of this save operation.
                if (!salarySettingExpansionSet.contains(salarySettingExpansion)) {

                    // Get the BCAF rows from the DB that are associated with the
                    // newly created salarySettingExpansion and attach so the
                    // method savePBGLSalarySetting() called below can get the total.
                    List<PendingBudgetConstructionAppointmentFunding> bcafRows = this.retrievePendingBudgetConstructionAppointmentFundings(salarySettingExpansion);
                    salarySettingExpansion.getPendingBudgetConstructionAppointmentFunding().addAll(bcafRows);
                    salarySettingExpansionSet.add(salarySettingExpansion);
                }
            }

            // collect the set of purge/notpurged SalarySettingExpansions here
            if (fundingLine.isPurged()) {
                purgedSseSet.add(salarySettingExpansion);
            } else {
                unpurgedSseSet.add(salarySettingExpansion);
            }

            // if SS by incumbent collect the set of purged/notpurged BudgetConstructionPositions here
            if (isSalarySettingByIncumbent) {
                BudgetConstructionPosition budgetConstructionPosition = fundingLine.getBudgetConstructionPosition();
                if (fundingLine.isPurged()) {
                    purgedBPOSNSet.add(budgetConstructionPosition);
                } else {
                    unpurgedBPOSNSet.add(budgetConstructionPosition);
                }
            }
        }

        // remove from set of purged SSEs the set of notpurged SSEs
        // leftover are those SSEs to release funding locks for after successful save
        purgedSseSet.removeAll(unpurgedSseSet);

        // if SS by incumbent, remove from set of purged BPOSNs the set of nonpurged BPOSNs
        if (isSalarySettingByIncumbent) {
            purgedBPOSNSet.removeAll(unpurgedBPOSNSet);
        }

        // Use the salarySettingExpansionSet to drive the update of PBGL rows (including any 2PLGs)
        for (SalarySettingExpansion salarySettingExpansion : salarySettingExpansionSet) {

            this.savePBGLSalarySetting(salarySettingExpansion);
        }

        // iterate leftover purged SSEs and release funding lock for each
        for (SalarySettingExpansion salarySettingExpansion : purgedSseSet) {
            String chartOfAccountsCode = salarySettingExpansion.getChartOfAccountsCode();
            String accountNumber = salarySettingExpansion.getAccountNumber();
            String subAccountNumber = salarySettingExpansion.getSubAccountNumber();
            Integer fiscalYear = salarySettingExpansion.getUniversityFiscalYear();
            String principalId = GlobalVariables.getUserSession().getPerson().getPrincipalId();

            // release the associated funding lock
            lockService.unlockFunding(chartOfAccountsCode, accountNumber, subAccountNumber, fiscalYear, principalId);

        }

        // if SS by incumbent iterate leftover purged BPOSNs and release position lock for each
        for (BudgetConstructionPosition budgetConstructionPosition : purgedBPOSNSet) {
            Person person = GlobalVariables.getUserSession().getPerson();
            lockService.unlockPostion(budgetConstructionPosition, person);
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#saveAppointmentFundings(java.util.List)
     */
    public void saveAppointmentFundings(List<PendingBudgetConstructionAppointmentFunding> appointmentFundings) {
        LOG.debug("saveAppointmentFundings() start");

        // remove the appointment funding lines being purged
        List<PendingBudgetConstructionAppointmentFunding> purgedAppointmentFundings = new ArrayList<PendingBudgetConstructionAppointmentFunding>();
        for (PendingBudgetConstructionAppointmentFunding appointmentFunding : appointmentFundings) {
            if (appointmentFunding.isPurged()) {
                purgedAppointmentFundings.add(appointmentFunding);
            }
        }
        this.purgeAppointmentFundings(purgedAppointmentFundings);

        // save the appointment funding lines that have been updated or newly created
        List<PendingBudgetConstructionAppointmentFunding> savableAppointmentFundings = new ArrayList<PendingBudgetConstructionAppointmentFunding>(appointmentFundings);
        savableAppointmentFundings.removeAll(purgedAppointmentFundings);

        // gwp - added this as part of double save optimistic exception fix
        // since savePBGLSalarySetting does not call this like saveSalarySetting does
        this.resetDeletedFundingLines(appointmentFundings);

        this.updateAppointmentFundingsBeforeSaving(savableAppointmentFundings);

        // save each line so deletion aware reasons get removed when needed
        for (PendingBudgetConstructionAppointmentFunding savableAppointmentFunding : savableAppointmentFundings) {
            businessObjectService.save(savableAppointmentFunding);
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#retriveSalarySalarySettingExpansion(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public SalarySettingExpansion retriveSalarySalarySettingExpansion(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        BudgetConstructionHeader budgetDocument = budgetDocumentService.getBudgetConstructionHeader(appointmentFunding);

        Map<String, Object> fieldValues = ObjectUtil.buildPropertyMap(appointmentFunding, SalarySettingExpansion.getPrimaryKeyFields());
        fieldValues.put(KFSPropertyConstants.DOCUMENT_NUMBER, budgetDocument.getDocumentNumber());

        return businessObjectService.findByPrimaryKey(SalarySettingExpansion.class, fieldValues);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#retrievePendingBudgetConstructionAppointmentFundings(org.kuali.kfs.module.bc.businessobject.SalarySettingExpansion)
     */
    public List<PendingBudgetConstructionAppointmentFunding> retrievePendingBudgetConstructionAppointmentFundings(SalarySettingExpansion salarySettingExpansion) {

        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, salarySettingExpansion.getUniversityFiscalYear());
        fieldValues.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, salarySettingExpansion.getChartOfAccountsCode());
        fieldValues.put(KFSPropertyConstants.ACCOUNT_NUMBER, salarySettingExpansion.getAccountNumber());
        fieldValues.put(KFSPropertyConstants.SUB_ACCOUNT_NUMBER, salarySettingExpansion.getSubAccountNumber());
        fieldValues.put(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, salarySettingExpansion.getFinancialObjectCode());
        fieldValues.put(KFSPropertyConstants.FINANCIAL_SUB_OBJECT_CODE, salarySettingExpansion.getFinancialSubObjectCode());

        return (List<PendingBudgetConstructionAppointmentFunding>) businessObjectService.findMatching(PendingBudgetConstructionAppointmentFunding.class, fieldValues);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#resetAppointmentFunding(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public void resetAppointmentFunding(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        appointmentFunding.setAppointmentRequestedAmount(KualiInteger.ZERO);
        appointmentFunding.setAppointmentRequestedTimePercent(BigDecimal.ZERO);
        appointmentFunding.setAppointmentRequestedPayRate(BigDecimal.ZERO);
        appointmentFunding.setAppointmentRequestedFteQuantity(BigDecimal.ZERO);

        appointmentFunding.setAppointmentRequestedCsfAmount(KualiInteger.ZERO);
        appointmentFunding.setAppointmentRequestedCsfFteQuantity(BigDecimal.ZERO);
        appointmentFunding.setAppointmentRequestedCsfTimePercent(BigDecimal.ZERO);

        appointmentFunding.setAppointmentTotalIntendedAmount(KualiInteger.ZERO);
        appointmentFunding.setAppointmentTotalIntendedFteQuantity(BigDecimal.ZERO);

        appointmentFunding.setAppointmentFundingDurationCode(BCConstants.AppointmentFundingDurationCodes.NONE.durationCode);

        appointmentFunding.setPositionObjectChangeIndicator(false);
        appointmentFunding.setPositionSalaryChangeIndicator(false);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#markAsDelete(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public void markAsDelete(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        this.resetAppointmentFunding(appointmentFunding);

        appointmentFunding.setAppointmentFundingDeleteIndicator(true);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#revert(java.util.List,
     * org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public void revert(List<PendingBudgetConstructionAppointmentFunding> appointmentFundings, PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        PendingBudgetConstructionAppointmentFunding vacantFunding = this.findVacantAppointmentFunding(appointmentFundings, appointmentFunding);

        if (vacantFunding != null) {
            appointmentFundings.remove(vacantFunding);
        }

        PendingBudgetConstructionAppointmentFunding newAppointmentFunding = (PendingBudgetConstructionAppointmentFunding) businessObjectService.retrieve(appointmentFunding);
        appointmentFundings.add(newAppointmentFunding);
        appointmentFundings.remove(appointmentFunding);
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#updateAccessOfAppointmentFunding(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding,
     * org.kuali.kfs.module.bc.util.SalarySettingFieldsHolder, boolean, java.util.Map, org.kuali.rice.kim.api.identity.Person)
     */
    public boolean updateAccessOfAppointmentFunding(PendingBudgetConstructionAppointmentFunding appointmentFunding, SalarySettingFieldsHolder salarySettingFieldsHolder, boolean budgetByObjectMode, boolean hasDocumentEditAccess, Person person) {
        String budgetChartOfAccountsCode = salarySettingFieldsHolder.getChartOfAccountsCode();
        String budgetAccountNumber = salarySettingFieldsHolder.getAccountNumber();
        String budgetSubAccountNumber = salarySettingFieldsHolder.getSubAccountNumber();
        String budgetObjectCode = salarySettingFieldsHolder.getFinancialObjectCode();
        String budgetSubObjectCode = salarySettingFieldsHolder.getFinancialSubObjectCode();

        String chartOfAccountsCode = appointmentFunding.getChartOfAccountsCode();
        String accountNumber = appointmentFunding.getAccountNumber();
        String subAccountNumber = appointmentFunding.getSubAccountNumber();
        String objectCode = appointmentFunding.getFinancialObjectCode();
        String subObjectCode = appointmentFunding.getFinancialSubObjectCode();

        // just allow edit if budget by object mode (general case of single account mode)
        if (budgetByObjectMode && StringUtils.equals(chartOfAccountsCode, budgetChartOfAccountsCode) && StringUtils.equals(accountNumber, budgetAccountNumber) && StringUtils.equals(subAccountNumber, budgetSubAccountNumber)) {
            // use the edit permission already calculated for the home account during document open
            appointmentFunding.setDisplayOnlyMode(!hasDocumentEditAccess);

            return true;
        }

        boolean isUpdatedByUserLevel = this.updateAccessOfAppointmentFundingByUserLevel(appointmentFunding, person);
        if (isUpdatedByUserLevel) {
            return true;
        }

        return false;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#updateAccessOfAppointmentFundingByUserLevel(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding,
     * org.kuali.rice.kim.api.identity.Person)
     */
    public boolean updateAccessOfAppointmentFundingByUserLevel(PendingBudgetConstructionAppointmentFunding appointmentFunding, Person user) {
        BudgetConstructionHeader budgetConstructionHeader = budgetDocumentService.getBudgetConstructionHeader(appointmentFunding);
        if (budgetConstructionHeader == null) {
            return false;
        }

        BudgetConstructionDocument document;
        try {
            document = (BudgetConstructionDocument) documentService.getByDocumentHeaderId(budgetConstructionHeader.getDocumentNumber());
        } catch (WorkflowException e) {
            throw new RuntimeException("Fail to retrieve budget document for doc id " + budgetConstructionHeader.getDocumentNumber());
        }

        TransactionalDocumentAuthorizer documentAuthorizer = (TransactionalDocumentAuthorizer) getDocumentHelperService().getDocumentAuthorizer(document);

        boolean hasEditAccess = documentAuthorizer.isAuthorized(document, BCConstants.BUDGET_CONSTRUCTION_NAMESPACE, BCConstants.KimApiConstants.EDIT_BCAF_PERMISSION_NAME, user.getPrincipalId());
        appointmentFunding.setDisplayOnlyMode(!hasEditAccess);

        boolean hasViewAmountsAccess = documentAuthorizer.isAuthorized(document, BCConstants.BUDGET_CONSTRUCTION_NAMESPACE, BCConstants.KimApiConstants.VIEW_BCAF_AMOUNTS_PERMISSION_NAME, user.getPrincipalId());
        appointmentFunding.setExcludedFromTotal(!hasViewAmountsAccess);

        return true;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#updateDerivedInformationForAppointmentFundings(java.util.List)
     */
    public void updateAppointmentFundingsBeforeSaving(List<PendingBudgetConstructionAppointmentFunding> appointmentFundings) {
        LOG.debug("updateDerivedInformationForAppointmentFundings() start");

        for (PendingBudgetConstructionAppointmentFunding appointmentFunding : appointmentFundings) {
            this.recalculateDerivedInformation(appointmentFunding);

            appointmentFunding.setNewLineIndicator(false);
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#updateDerivedInformationForAppointmentFunding(org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding)
     */
    public void recalculateDerivedInformation(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        this.preprocessFundingReason(appointmentFunding);
        this.preprocessLeaveRequest(appointmentFunding);

        boolean isHourlyPaid = this.isHourlyPaid(appointmentFunding);
        appointmentFunding.setHourlyPaid(isHourlyPaid);

        if (appointmentFunding.isHourlyPaid()) {
            this.normalizePayRateAndAmount(appointmentFunding);
        } else {
            appointmentFunding.setAppointmentRequestedPayRate(BigDecimal.ZERO);
        }

        BigDecimal requestedFteQuantity = this.calculateFteQuantityFromAppointmentFunding(appointmentFunding);
        appointmentFunding.setAppointmentRequestedFteQuantity(requestedFteQuantity);

        if (!appointmentFunding.getAppointmentFundingDurationCode().equals(NONE.durationCode)) {
            BigDecimal requestedCSFFteQuantity = this.calculateCSFFteQuantityFromAppointmentFunding(appointmentFunding);
            appointmentFunding.setAppointmentRequestedCsfFteQuantity(requestedCSFFteQuantity);
        }
    }

    /**
     * reset the amount values of each line in the given appointment fundings as zeros and remove the reason annotations if the line
     * is marked as deleted
     *
     * @param pendingBudgetConstructionAppointmentFunding the given appointment fundings
     */
    protected void resetDeletedFundingLines(List<PendingBudgetConstructionAppointmentFunding> pendingBudgetConstructionAppointmentFunding) {
        for (PendingBudgetConstructionAppointmentFunding appointmentFunding : pendingBudgetConstructionAppointmentFunding) {
            if (!appointmentFunding.isAppointmentFundingDeleteIndicator() || appointmentFunding.isPersistedDeleteIndicator()) {
                continue;
            }

            this.markAsDelete(appointmentFunding);
            List<BudgetConstructionAppointmentFundingReason> reasons = appointmentFunding.getBudgetConstructionAppointmentFundingReason();
            if (reasons != null) {
                reasons.clear();
            }

            appointmentFunding.setPersistedDeleteIndicator(true);
        }
    }

    /**
     * get the csf tracker amount of the given appointment funding
     *
     * @param appointmentFunding the given appointment funding
     * @return the csf tracker amount of the given appointment funding if any; otherwise, return zero
     */
    protected KualiInteger getCsfAmount(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        if (appointmentFunding == null) {
            return KualiInteger.ZERO;
        }

        BudgetConstructionCalculatedSalaryFoundationTracker csfTracker = appointmentFunding.getEffectiveCSFTracker();
        if (csfTracker == null) {
            return KualiInteger.ZERO;
        }

        return csfTracker.getCsfAmount();
    }

    /**
     * determine whether there exists at lease one vacant funding line for the given appointment funding
     *
     * @param appointmentFunding the given appointment funding
     * @return true if there exists at lease one vacant funding line for the given appointment funding; otherwise, return false
     */
    protected boolean hasBeenVacated(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        Map<String, Object> keyFieldValues = appointmentFunding.getValuesMap();
        keyFieldValues.put(KFSPropertyConstants.EMPLID, BCConstants.VACANT_EMPLID);

        return businessObjectService.countMatching(PendingBudgetConstructionAppointmentFunding.class, keyFieldValues) > 0;
    }

    /**
     * create a vacant appointment funding based on the given budget funding
     *
     * @param appointmentFunding the given appointment funding
     * @return a vacant appointment funding
     */
    protected PendingBudgetConstructionAppointmentFunding createVacantAppointmentFunding(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        PendingBudgetConstructionAppointmentFunding vacantAppointmentFunding = new PendingBudgetConstructionAppointmentFunding();

        ObjectUtil.buildObjectWithoutReferenceFields(vacantAppointmentFunding, appointmentFunding);
        vacantAppointmentFunding.setEmplid(BCConstants.VACANT_EMPLID);
        vacantAppointmentFunding.setAppointmentFundingDeleteIndicator(false);
        vacantAppointmentFunding.setPersistedDeleteIndicator(false);
        vacantAppointmentFunding.setVersionNumber(null);
        vacantAppointmentFunding.setNewLineIndicator(true);

        return vacantAppointmentFunding;
    }

    /**
     * create a pseudo appointment funding for the salary setting expansion this is used when there are no funding lines for the
     * salary setting expansion to get a funding line to be used to pass primary key info
     *
     * @param salarySettingExpansion
     * @return a pseudo appointment funding
     */
    protected PendingBudgetConstructionAppointmentFunding createPseudoAppointmentFundingLine(SalarySettingExpansion salarySettingExpansion) {
        PendingBudgetConstructionAppointmentFunding pseudoAppointmentFunding = new PendingBudgetConstructionAppointmentFunding();

        pseudoAppointmentFunding.setUniversityFiscalYear(salarySettingExpansion.getUniversityFiscalYear());
        pseudoAppointmentFunding.setChartOfAccountsCode(salarySettingExpansion.getChartOfAccountsCode());
        pseudoAppointmentFunding.setAccountNumber(salarySettingExpansion.getAccountNumber());
        pseudoAppointmentFunding.setSubAccountNumber(salarySettingExpansion.getSubAccountNumber());
        pseudoAppointmentFunding.setFinancialObjectCode(salarySettingExpansion.getFinancialObjectCode());
        pseudoAppointmentFunding.setFinancialSubObjectCode(salarySettingExpansion.getFinancialSubObjectCode());
        pseudoAppointmentFunding.setAppointmentFundingDeleteIndicator(false);
        pseudoAppointmentFunding.setAppointmentRequestedAmount(KualiInteger.ZERO);
        pseudoAppointmentFunding.refreshReferenceObject(KFSPropertyConstants.ACCOUNT);

        return pseudoAppointmentFunding;
    }

    /**
     * preprocess the funding reason of the given appointment funding before the funding is saved
     *
     * @param appointmentFunding the given appointment funding
     */
    public void preprocessFundingReason(PendingBudgetConstructionAppointmentFunding appointmentFunding) {

        List<BudgetConstructionAppointmentFundingReason> fundingReasons = appointmentFunding.getBudgetConstructionAppointmentFundingReason();

        // do special removal of any reason rows where the reason code is blank
        if (!fundingReasons.isEmpty() && StringUtils.isBlank(fundingReasons.get(0).getAppointmentFundingReasonCode())) {
            fundingReasons.clear();
        }
    }

    /**
     * preprocess the leave request of the given appointment funding before the funding is saved
     *
     * @param appointmentFunding the given appointment funding
     */
    public void preprocessLeaveRequest(PendingBudgetConstructionAppointmentFunding appointmentFunding) {
        String durationCode = appointmentFunding.getAppointmentFundingDurationCode();

        if (StringUtils.isEmpty(durationCode) || StringUtils.equals(durationCode, BCConstants.AppointmentFundingDurationCodes.NONE.durationCode)) {
            appointmentFunding.setAppointmentRequestedCsfAmount(KualiInteger.ZERO);
            appointmentFunding.setAppointmentRequestedCsfFteQuantity(BigDecimal.ZERO);
            appointmentFunding.setAppointmentRequestedCsfTimePercent(BigDecimal.ZERO);
        }
    }

    /**
     * @see org.kuali.kfs.module.bc.document.service.SalarySettingService#hasExistingFundingReason(org.kuali.kfs.module.bc.businessobject.BudgetConstructionAppointmentFundingReasonCode)
     */
    public boolean hasExistingFundingReason(BudgetConstructionAppointmentFundingReasonCode budgetConstructionAppointmentFundingReasonCode) {

        Map<String, Object> queryMap = new HashMap<String, Object>();
        queryMap.put("appointmentFundingReasonCode", budgetConstructionAppointmentFundingReasonCode.getAppointmentFundingReasonCode());

        return (businessObjectService.countMatching(BudgetConstructionAppointmentFundingReason.class, queryMap) > 0);
    }

    /**
     * Sets the kualiConfigurationService attribute value.
     *
     * @param kualiConfigurationService The kualiConfigurationService to set.
     */
    public void setConfigurationService(ConfigurationService kualiConfigurationService) {
        this.kualiConfigurationService = kualiConfigurationService;
    }

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

    /**
     * Sets the laborModuleService attribute value.
     *
     * @param laborModuleService The laborModuleService to set.
     */
    public void setLaborModuleService(LaborModuleService laborModuleService) {
        this.laborModuleService = laborModuleService;
    }

    /**
     * Sets the budgetDocumentService attribute value.
     *
     * @param budgetDocumentService The budgetDocumentService to set.
     */
    public void setBudgetDocumentService(BudgetDocumentService budgetDocumentService) {
        this.budgetDocumentService = budgetDocumentService;
    }

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

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

    /**
     * Sets the lockService attribute value.
     *
     * @param lockService The lockService to set.
     */
    public void setLockService(LockService lockService) {
        this.lockService = lockService;
    }

    /**
     * Gets the documentHelperService attribute.
     *
     * @return Returns the documentHelperService.
     */
    public DocumentHelperService getDocumentHelperService() {
        if (documentHelperService == null) {
            documentHelperService = SpringContext.getBean(DocumentHelperService.class);
        }
        return documentHelperService;
    }

    /**
     * Sets the documentHelperService attribute value.
     *
     * @param documentHelperService The documentHelperService to set.
     */
    public void setDocumentHelperService(DocumentHelperService documentHelperService) {
        this.documentHelperService = documentHelperService;
    }

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

    /**
     * Sets the budgetConstructionProcessorService attribute value.
     *
     * @param budgetConstructionProcessorService The budgetConstructionProcessorService to set.
     */
    public void setBudgetConstructionProcessorService(BudgetConstructionProcessorService budgetConstructionProcessorService) {
        this.budgetConstructionProcessorService = budgetConstructionProcessorService;
    }
}
