org.kuali.kfs.module.bc.document.validation.impl.BudgetConstructionDocumentRules.java Source code

Java tutorial

Introduction

Here is the source code for org.kuali.kfs.module.bc.document.validation.impl.BudgetConstructionDocumentRules.java

Source

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

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.PropertyUtils;
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.ObjectCode;
import org.kuali.kfs.coa.businessobject.SubAccount;
import org.kuali.kfs.coa.businessobject.SubObjectCode;
import org.kuali.kfs.fp.service.FiscalYearFunctionControlService;
import org.kuali.kfs.module.bc.BCConstants;
import org.kuali.kfs.module.bc.BCKeyConstants;
import org.kuali.kfs.module.bc.BCPropertyConstants;
import org.kuali.kfs.module.bc.BCConstants.AccountSalarySettingOnlyCause;
import org.kuali.kfs.module.bc.BCConstants.MonthSpreadDeleteType;
import org.kuali.kfs.module.bc.businessobject.BudgetConstructionMonthly;
import org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionGeneralLedger;
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.BudgetDocumentService;
import org.kuali.kfs.module.bc.document.service.BudgetParameterService;
import org.kuali.kfs.module.bc.document.service.SalarySettingService;
import org.kuali.kfs.module.bc.document.validation.AddBudgetConstructionDocumentRule;
import org.kuali.kfs.module.bc.document.validation.AddPendingBudgetGeneralLedgerLineRule;
import org.kuali.kfs.module.bc.document.validation.DeleteMonthlySpreadRule;
import org.kuali.kfs.module.bc.document.validation.DeletePendingBudgetGeneralLedgerLineRule;
import org.kuali.kfs.module.bc.document.validation.SaveMonthlyBudgetRule;
import org.kuali.kfs.module.bc.util.BudgetParameterFinder;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.service.AccountingLineRuleHelperService;
import org.kuali.rice.core.api.util.type.KualiInteger;
import org.kuali.rice.core.api.util.type.TypeUtils;
import org.kuali.rice.kns.rules.TransactionalDocumentRuleBase;
import org.kuali.rice.kns.service.DataDictionaryService;
import org.kuali.rice.kns.service.DictionaryValidationService;
import org.kuali.rice.kns.util.KNSGlobalVariables;
import org.kuali.rice.krad.datadictionary.DataDictionary;
import org.kuali.rice.krad.document.Document;
import org.kuali.rice.krad.exception.InfrastructureException;
import org.kuali.rice.krad.service.BusinessObjectService;
import org.kuali.rice.krad.service.PersistenceService;
import org.kuali.rice.krad.util.GlobalVariables;
import org.kuali.rice.krad.util.KRADConstants;
import org.kuali.rice.krad.util.MessageMap;
import org.kuali.rice.krad.util.ObjectUtils;

public class BudgetConstructionDocumentRules extends TransactionalDocumentRuleBase
        implements AddBudgetConstructionDocumentRule<BudgetConstructionDocument>,
        AddPendingBudgetGeneralLedgerLineRule<BudgetConstructionDocument, PendingBudgetConstructionGeneralLedger>,
        DeletePendingBudgetGeneralLedgerLineRule<BudgetConstructionDocument, PendingBudgetConstructionGeneralLedger>,
        DeleteMonthlySpreadRule<BudgetConstructionDocument>,
        SaveMonthlyBudgetRule<BudgetConstructionDocument, BudgetConstructionMonthly> {
    protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger
            .getLogger(BudgetConstructionDocumentRules.class);

    // some services used here - other service refs are from parent classes
    // if this class is extended we may need to create protected getters
    protected static BudgetParameterService budgetParameterService = SpringContext
            .getBean(BudgetParameterService.class);
    protected static AccountingLineRuleHelperService accountingLineRuleHelper = SpringContext
            .getBean(AccountingLineRuleHelperService.class);
    protected static DataDictionaryService dataDictionaryService = SpringContext
            .getBean(DataDictionaryService.class);
    protected static SalarySettingService salarySettingService = SpringContext.getBean(SalarySettingService.class);
    protected static BusinessObjectService businessObjectService = SpringContext
            .getBean(BusinessObjectService.class);
    protected static FiscalYearFunctionControlService fiscalYearFunctionControlService = SpringContext
            .getBean(FiscalYearFunctionControlService.class);

    protected Collection<String> revenueObjectTypesParamValues = BudgetParameterFinder.getRevenueObjectTypes();
    protected Collection<String> expenditureObjectTypesParamValues = BudgetParameterFinder
            .getExpenditureObjectTypes();
    protected Collection<String> budgetAggregationCodesParamValues = BudgetParameterFinder
            .getBudgetAggregationCodes();
    protected Collection<String> fringeBenefitDesignatorCodesParamValues = BudgetParameterFinder
            .getFringeBenefitDesignatorCodes();
    protected Collection<String> salarySettingFundGroupsParamValues = BudgetParameterFinder
            .getSalarySettingFundGroups();
    protected Collection<String> salarySettingSubFundGroupsParamValues = BudgetParameterFinder
            .getSalarySettingSubFundGroups();

    // this field is highlighted for any errors found on an existing line
    protected static final String TARGET_ERROR_PROPERTY_NAME = KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT;

    public BudgetConstructionDocumentRules() {
        super();
    }

    /**
     * @see org.kuali.kfs.module.bc.document.validation.AddBudgetConstructionDocumentRule#processAddBudgetConstructionDocumentRules(org.kuali.kfs.module.bc.document.BudgetConstructionDocument)
     */
    @Override
    public boolean processAddBudgetConstructionDocumentRules(
            BudgetConstructionDocument budgetConstructionDocument) {
        LOG.debug("processAddBudgetConstructionDocumentRules(Document) - start");

        MessageMap errors = GlobalVariables.getMessageMap();
        boolean isValid = true;

        // validate primitives for required field and formatting checks
        int originalErrorCount = errors.getErrorCount();
        SpringContext.getBean(DictionaryValidationService.class).validateBusinessObject(budgetConstructionDocument);

        // check to see if any errors were reported
        int currentErrorCount = errors.getErrorCount();
        isValid &= (currentErrorCount == originalErrorCount);
        if (!isValid) {
            return isValid;
        }

        // can't create BC documents when in system view only mode
        // let the user know this up front
        if (!fiscalYearFunctionControlService
                .isBudgetUpdateAllowed(budgetConstructionDocument.getUniversityFiscalYear())) {
            errors.putError(KFSPropertyConstants.ACCOUNT_NUMBER, BCKeyConstants.MESSAGE_BUDGET_SYSTEM_VIEW_ONLY);
            isValid &= false;
        }

        // check existence of account first
        DataDictionary dd = dataDictionaryService.getDataDictionary();
        String pkeyValue = budgetConstructionDocument.getChartOfAccountsCode() + "-"
                + budgetConstructionDocument.getAccountNumber();
        isValid &= isValidAccount(budgetConstructionDocument.getAccount(), pkeyValue, dd,
                KFSPropertyConstants.ACCOUNT_NUMBER);
        if (isValid) {

            // run the rules checks preventing BC document creation - assumes account exists
            isValid &= this.isBudgetAllowed(budgetConstructionDocument, KFSPropertyConstants.ACCOUNT_NUMBER, errors,
                    true, true);
        }

        if (!isValid) {

            // tell the user we can't create a new BC document along with the error reasons
            KNSGlobalVariables.getMessageList().add(BCKeyConstants.MESSAGE_BUDGET_NOCREATE_DOCUMENT);
        }

        LOG.debug("processAddBudgetConstructionDocumentRules(Document) - end");
        return isValid;
    }

    /**
     * Runs business rules prior to saving Budget Document proper. This is different than saving typical KFS documents in that the
     * document is not saved to the user's inbox. Saved Budget Documents must meet the same state requirements as the typical KFS
     * routed document, so required field checks must be done. Budget Documents can be opened by a user in edit mode multiple times
     * and while in edit mode documents can be pushed down the review hierarchy, monthly budgets and appointment funding updated,
     * benefits calculated, etc. Each of these operations require the document's data be in a consistent state with respect to
     * business rules before the operation be performed.
     *
     * @see org.kuali.rice.krad.rules.DocumentRuleBase#processSaveDocument(org.kuali.rice.krad.document.Document)
     */
    @Override
    public boolean processSaveDocument(Document document) {
        LOG.debug("processSaveDocument(Document) - start");

        boolean isValid = true;

        // run through the attributes recursively and check dd stuff
        isValid &= isDocumentAttributesValid(document, true);

        if (isValid) {
            isValid &= processSaveBudgetDocumentRules((BudgetConstructionDocument) document,
                    MonthSpreadDeleteType.NONE);
        }

        // no custom save rules since we are overriding and doing what we want here already

        LOG.debug("processSaveDocument(Document) - end");
        return isValid;
    }

    @Override
    public boolean processDeleteMonthlySpreadRules(BudgetConstructionDocument budgetConstructionDocument,
            MonthSpreadDeleteType monthSpreadDeleteType) {
        LOG.debug("processDeleteRevenueMonthlySpreadRules(Document) - start");

        boolean isValid = true;

        // run through the attributes recursively and check dd stuff
        isValid &= isDocumentAttributesValid(budgetConstructionDocument, true);

        if (isValid) {
            isValid &= processSaveBudgetDocumentRules(budgetConstructionDocument, monthSpreadDeleteType);
        }

        // no custom save rules since we are overriding and doing what we want here already

        LOG.debug("processDeleteRevenueMonthlySpreadRules(Document) - end");
        return isValid;

    }

    /**
     * Iterates through existing revenue and expenditure lines to do validation, ri checks on object/sub-object code and request
     * amount referential integrity checks against appointment funding and monthly detail amounts. Checks are performed when the
     * request amount has been updated, since initial add action, the last save event or since opening the document, whatever is
     * latest.
     *
     * @see org.kuali.module.budget.rule.SaveBudgetDocumentRule#processSaveBudgetDocumentRules(D)
     */
    public boolean processSaveBudgetDocumentRules(BudgetConstructionDocument budgetConstructionDocument,
            MonthSpreadDeleteType monthSpreadDeleteType) {

        MessageMap errors = GlobalVariables.getMessageMap();
        boolean doRevMonthRICheck = true;
        boolean doExpMonthRICheck = true;
        boolean isValid = true;
        int originalErrorCount;
        int currentErrorCount;

        // refresh only the doc refs we need
        List refreshFields = Collections.unmodifiableList(
                Arrays.asList(new String[] { KFSPropertyConstants.ACCOUNT, KFSPropertyConstants.SUB_ACCOUNT }));
        SpringContext.getBean(PersistenceService.class).retrieveReferenceObjects(budgetConstructionDocument,
                refreshFields);

        errors.addToErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);

        if (monthSpreadDeleteType == MonthSpreadDeleteType.REVENUE) {
            doRevMonthRICheck = false;
            doExpMonthRICheck = true;
        } else {
            if (monthSpreadDeleteType == MonthSpreadDeleteType.EXPENDITURE) {
                doRevMonthRICheck = true;
                doExpMonthRICheck = false;
            }
        }

        // iterate and validate revenue lines
        isValid &= this.checkPendingBudgetConstructionGeneralLedgerLines(budgetConstructionDocument, errors, true,
                doRevMonthRICheck);

        // iterate and validate expenditure lines
        isValid &= this.checkPendingBudgetConstructionGeneralLedgerLines(budgetConstructionDocument, errors, false,
                doExpMonthRICheck);

        errors.removeFromErrorPath(KRADConstants.DOCUMENT_PROPERTY_NAME);

        return isValid;
    }

    /**
     * Checks a new PBGL line. Comprehensive checks are done.
     *
     * @param budgetConstructionDocument
     * @param pendingBudgetConstructionGeneralLedger
     * @param isRevenue
     * @return
     */
    @Override
    public boolean processAddPendingBudgetGeneralLedgerLineRules(
            BudgetConstructionDocument budgetConstructionDocument,
            PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger, boolean isRevenue) {
        LOG.debug("processAddPendingBudgetGeneralLedgerLineRules() start");

        // List refreshFields;
        MessageMap errors = GlobalVariables.getMessageMap();
        boolean isValid = true;

        int originalErrorCount = errors.getErrorCount();

        // validate primitives for required field and formatting checks
        SpringContext.getBean(DictionaryValidationService.class)
                .validateBusinessObject(pendingBudgetConstructionGeneralLedger);

        // check to see if any errors were reported
        int currentErrorCount = errors.getErrorCount();
        isValid &= (currentErrorCount == originalErrorCount);

        if (isValid) {

            // refresh only the doc refs we need
            List refreshFields = Collections.unmodifiableList(
                    Arrays.asList(new String[] { KFSPropertyConstants.ACCOUNT, KFSPropertyConstants.SUB_ACCOUNT }));
            SpringContext.getBean(PersistenceService.class).retrieveReferenceObjects(budgetConstructionDocument,
                    refreshFields);
            // budgetConstructionDocument.getSubAccount().refreshReferenceObject(KFSPropertyConstants.A21_SUB_ACCOUNT);

            isValid &= this.checkPendingBudgetConstructionGeneralLedgerLine(budgetConstructionDocument,
                    pendingBudgetConstructionGeneralLedger, errors, isRevenue, true);

            if (isValid) {
                // line checks ok - does line already exist in target revenue or expenditure list
                isValid &= isNewLineUnique(budgetConstructionDocument, pendingBudgetConstructionGeneralLedger,
                        errors, isRevenue);
            }
        }

        if (!isValid) {
            LOG.info(
                    "business rule checks failed in processAddPendingBudgetGeneralLedgerLineRules in BudgetConstructionRules");
        }

        LOG.debug("processAddPendingBudgetGeneralLedgerLineRules() end");
        return isValid;
    }

    /**
     * Runs rules for deleting an existing revenue or expenditure line.
     *
     * @param budgetConstructionDocument
     * @param pendingBudgetConstructionGeneralLedger
     * @param isRevenue
     * @return
     */
    @Override
    public boolean processDeletePendingBudgetGeneralLedgerLineRules(
            BudgetConstructionDocument budgetConstructionDocument,
            PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger, boolean isRevenue) {
        LOG.debug("processDeletePendingBudgetGeneralLedgerLineRules() start");

        MessageMap errors = GlobalVariables.getMessageMap();
        boolean isValid = true;

        // no delete allowed if base exists, the delete button shouldn't even exist in this case, but checking anyway
        if (pendingBudgetConstructionGeneralLedger.getFinancialBeginningBalanceLineAmount().isZero()) {
            isValid &= true;
        } else {
            isValid &= false;
            String pkeyVal = pendingBudgetConstructionGeneralLedger.getFinancialObjectCode() + ","
                    + pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode();
            GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT,
                    BCKeyConstants.ERROR_NO_DELETE_ALLOWED_WITH_BASE, pkeyVal);
        }

        if (!isRevenue) {
            // no lines using fringe benefit target object codes allowed to be manually deleted by user
            // the lines are created by benefits calculation process
            // again the delete button shouldn't even exist
            isValid &= isNotFringeBenefitObject(fringeBenefitDesignatorCodesParamValues,
                    pendingBudgetConstructionGeneralLedger, errors, false);

            // no deletion if salary setting option is turned on
            // and the line is a salary detail line and detail recs exist
            if (!SpringContext.getBean(SalarySettingService.class).isSalarySettingDisabled()) {
                if (pendingBudgetConstructionGeneralLedger.getLaborObject() != null) {
                    if (pendingBudgetConstructionGeneralLedger.getLaborObject()
                            .isDetailPositionRequiredIndicator()) {
                        if (pendingBudgetConstructionGeneralLedger
                                .isPendingBudgetConstructionAppointmentFundingExists()) {
                            isValid &= false;
                            String pkeyVal = pendingBudgetConstructionGeneralLedger.getFinancialObjectCode() + ","
                                    + pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode();
                            GlobalVariables.getMessageMap().putError(
                                    KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT,
                                    BCKeyConstants.ERROR_NO_DELETE_ALLOWED_SALARY_DETAIL, pkeyVal);
                        }
                    }
                }
            }
            if (!SpringContext.getBean(BenefitsCalculationService.class).isBenefitsCalculationDisabled()) {

                // benefits calc is turned on, if the line is valid to remove and the request is not zero, set to calc benefits
                if (isValid && pendingBudgetConstructionGeneralLedger.getPositionObjectBenefit() != null
                        && !pendingBudgetConstructionGeneralLedger.getPositionObjectBenefit().isEmpty()) {
                    budgetConstructionDocument.setBenefitsCalcNeeded(true);

                    // test if the line has monthly budgets
                    // this assumes business rule of non-zero monthly budget not allowed to sum to a zero annual amount
                    // that is, if annual amount is zero, the monthly record contains all zeros
                    if (pendingBudgetConstructionGeneralLedger.getBudgetConstructionMonthly() != null
                            && !pendingBudgetConstructionGeneralLedger.getBudgetConstructionMonthly().isEmpty()) {
                        budgetConstructionDocument.setMonthlyBenefitsCalcNeeded(true);
                    }
                }
            }
        }

        LOG.debug("processDeletePendingBudgetGeneralLedgerLineRules() end");
        return isValid;
    }

    /**
     * @see org.kuali.kfs.module.bc.document.validation.SaveMonthlyBudgetRule#processSaveMonthlyBudgetRules(org.kuali.kfs.module.bc.document.BudgetConstructionDocument,
     *      org.kuali.kfs.module.bc.businessobject.BudgetConstructionMonthly)
     */
    @Override
    public boolean processSaveMonthlyBudgetRules(BudgetConstructionDocument budgetConstructionDocument,
            BudgetConstructionMonthly budgetConstructionMonthly) {
        LOG.debug("processSaveMonthlyBudgetRules() start");

        budgetConstructionMonthly.refreshNonUpdateableReferences();
        PendingBudgetConstructionGeneralLedger pbgl = budgetConstructionMonthly
                .getPendingBudgetConstructionGeneralLedger();
        MessageMap errors = GlobalVariables.getMessageMap();
        boolean isValid = true;

        int originalErrorCount = errors.getErrorCount();

        // validate primitives for required field and formatting checks
        SpringContext.getBean(DictionaryValidationService.class).validateBusinessObject(budgetConstructionMonthly);

        // check to see if any errors were reported
        int currentErrorCount = errors.getErrorCount();
        isValid &= (currentErrorCount == originalErrorCount);

        // Check special cleanup mode case and berate user on save of anything.
        // The user should delete the row, which bypasses this rule.
        if (!budgetConstructionDocument.isBudgetableDocument()) {
            isValid &= Boolean.FALSE;
            errors.putError(BCPropertyConstants.FINANCIAL_DOCUMENT_MONTH1_LINE_AMOUNT,
                    BCKeyConstants.ERROR_BUDGET_DOCUMENT_NOT_BUDGETABLE,
                    budgetConstructionDocument.getAccountNumber() + ";"
                            + budgetConstructionDocument.getSubAccountNumber());
        }

        DataDictionary dd = dataDictionaryService.getDataDictionary();
        if (isValid) {

            ObjectCode objectCode = budgetConstructionMonthly.getFinancialObject();
            isValid &= isValidObjectCode(objectCode, budgetConstructionMonthly.getFinancialObjectCode(), dd,
                    KFSPropertyConstants.FINANCIAL_OBJECT_CODE);

            if (StringUtils.isNotBlank(budgetConstructionMonthly.getFinancialSubObjectCode())
                    && !budgetConstructionMonthly.getFinancialSubObjectCode()
                            .equalsIgnoreCase(KFSConstants.getDashFinancialSubObjectCode())) {
                SubObjectCode subObjectCode = budgetConstructionMonthly.getFinancialSubObject();
                isValid &= isValidSubObjectCode(subObjectCode,
                        budgetConstructionMonthly.getFinancialSubObjectCode(), dd,
                        KFSPropertyConstants.FINANCIAL_SUB_OBJECT_CODE);
            }
        }

        if (isValid) {
            KualiInteger monthlyTotal = budgetConstructionMonthly.getFinancialDocumentMonthTotalLineAmount();
            if (!salarySettingService.isSalarySettingDisabled()) {
                if (pbgl.getLaborObject() != null && pbgl.getLaborObject().isDetailPositionRequiredIndicator()) {

                    // no request amount overrides allowed for salary setting detail lines
                    if (!monthlyTotal.equals(pbgl.getAccountLineAnnualBalanceAmount())) {
                        isValid &= false;
                        errors.putError(BCPropertyConstants.FINANCIAL_DOCUMENT_MONTH1_LINE_AMOUNT,
                                BCKeyConstants.ERROR_MONTHLY_DETAIL_SALARY_OVERIDE,
                                budgetConstructionMonthly.getFinancialObjectCode(), monthlyTotal.toString(),
                                pbgl.getAccountLineAnnualBalanceAmount().toString());
                    }
                }
            }

            // check for monthly total adding to zero (makes no sense)
            if (monthlyTotal.isZero()) {
                boolean nonZeroMonthlyExists = false;
                nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth1LineAmount()
                        .isNonZero();
                nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth2LineAmount()
                        .isNonZero();
                nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth3LineAmount()
                        .isNonZero();
                nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth4LineAmount()
                        .isNonZero();
                nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth5LineAmount()
                        .isNonZero();
                nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth6LineAmount()
                        .isNonZero();
                nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth7LineAmount()
                        .isNonZero();
                nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth8LineAmount()
                        .isNonZero();
                nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth9LineAmount()
                        .isNonZero();
                nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth10LineAmount()
                        .isNonZero();
                nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth11LineAmount()
                        .isNonZero();
                nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth12LineAmount()
                        .isNonZero();
                if (nonZeroMonthlyExists) {
                    isValid &= false;
                    errors.putError(BCPropertyConstants.FINANCIAL_DOCUMENT_MONTH1_LINE_AMOUNT,
                            BCKeyConstants.ERROR_MONTHLY_TOTAL_ZERO);
                }
            }
        } else {
            LOG.info(
                    "business rule checks failed in processSaveMonthlyBudgetRules in BudgetConstructionDocumentRules");
        }

        LOG.debug("processSaveMonthlyBudgetRules() end");
        return isValid;
    }

    /**
     * Iterates existing revenue or expenditure lines. Checks if request amount is non-zero or has changed and runs business rules
     * on the line.
     *
     * @param budgetConstructionDocument
     * @param errors
     * @param isRevenue
     * @return
     */
    protected boolean checkPendingBudgetConstructionGeneralLedgerLines(
            BudgetConstructionDocument budgetConstructionDocument, MessageMap errors, boolean isRevenue,
            boolean doMonthRICheck) {

        boolean isValid = true;
        boolean isReqAmountValid;
        int originalErrorCount;
        int currentErrorCount;
        List<PendingBudgetConstructionGeneralLedger> pendingBudgetConstructionGeneralLedgerLines;
        String linesErrorPath;

        if (isRevenue) {
            pendingBudgetConstructionGeneralLedgerLines = budgetConstructionDocument
                    .getPendingBudgetConstructionGeneralLedgerRevenueLines();
            linesErrorPath = BCPropertyConstants.PENDING_BUDGET_CONSTRUCTION_GENERAL_LEDGER_REVENUE_LINES;
        } else {
            pendingBudgetConstructionGeneralLedgerLines = budgetConstructionDocument
                    .getPendingBudgetConstructionGeneralLedgerExpenditureLines();
            linesErrorPath = BCPropertyConstants.PENDING_BUDGET_CONSTRUCTION_GENERAL_LEDGER_EXPENDITURE_LINES;
        }

        // iterate revenue or expenditure lines
        Integer index = 0;
        for (Iterator iter = pendingBudgetConstructionGeneralLedgerLines.iterator(); iter.hasNext(); index++) {

            PendingBudgetConstructionGeneralLedger element = (PendingBudgetConstructionGeneralLedger) iter.next();
            errors.addToErrorPath(linesErrorPath + "[" + index + "]");

            originalErrorCount = errors.getErrorCount();

            // run dd required field and format checks on request amount only, since only it can be changed by user
            // no sanity checks on hiddens and readonly field params
            validatePrimitiveFromDescriptor(element, TARGET_ERROR_PROPERTY_NAME, "", true);

            // check to see if any errors were reported
            currentErrorCount = errors.getErrorCount();
            isReqAmountValid = (currentErrorCount == originalErrorCount);
            isValid &= isReqAmountValid;

            // test for new errors from this point - if none, test if benefits calc required
            originalErrorCount = errors.getErrorCount();

            // has the request amount changed?
            boolean isRequestAmountChanged = (isReqAmountValid && (!element.getAccountLineAnnualBalanceAmount()
                    .equals(element.getPersistedAccountLineAnnualBalanceAmount())));

            // only do checks if request amount is non-zero and not equal to currently persisted amount
            // or the document is not budgetable and the request is non-zero
            if (isReqAmountValid && element.getAccountLineAnnualBalanceAmount().isNonZero()) {

                boolean isSalaryFringeLine = false;
                if (!isRevenue && fringeBenefitDesignatorCodesParamValues != null
                        && element.getLaborObject() != null) {
                    isSalaryFringeLine = fringeBenefitDesignatorCodesParamValues
                            .contains(element.getLaborObject().getFinancialObjectFringeOrSalaryCode());
                }
                boolean is2PLG = !isRevenue && element.getFinancialObjectCode()
                        .contentEquals(KFSConstants.BudgetConstructionConstants.OBJECT_CODE_2PLG);
                boolean isCleanupModeActionForceCheck = budgetConstructionDocument.isCleanupModeActionForceCheck();

                // Request notZero, do checks if user enters a change to a request amount or
                // (We are in cleanupMode and the current action (save or close-save) forces a cleanup mode check and
                // not 2PLG line and not salary fringe line)
                // This allows the user to use quick salary setting, monthly edit, global month delete to do cleanup work and
                // to print out values or push/pull before cleanup.
                if (isRequestAmountChanged || (doMonthRICheck && !is2PLG && !isSalaryFringeLine)
                        || (!budgetConstructionDocument.isBudgetableDocument() && isCleanupModeActionForceCheck
                                && !is2PLG && !isSalaryFringeLine)) {
                    isValid &= this.checkPendingBudgetConstructionGeneralLedgerLine(budgetConstructionDocument,
                            element, errors, isRevenue, false);
                }
            }

            // Do RI type checks for request amount against monthly and salary setting detail if persisted amount changes
            // or a 2plg exists and the line is a salary setting detail line
            // Also tests if the line is has benefits associate and flags that a benefits calculation needs done.
            // Benefits calc is then called in the form action after successful rules check and save
            boolean forceTwoPlugRICheck = (budgetConstructionDocument.isContainsTwoPlug()
                    && (element.getLaborObject() != null
                            && element.getLaborObject().isDetailPositionRequiredIndicator()));

            // force monthly RI check if 2PLG and if request amount changes AND not a detail salary setting line
            boolean forceMonthlyRICheck = (budgetConstructionDocument.isContainsTwoPlug()
                    && (element.getLaborObject() == null
                            || !element.getLaborObject().isDetailPositionRequiredIndicator()));

            if (isReqAmountValid && (isRequestAmountChanged || forceTwoPlugRICheck)) {

                // check monthly for all rows
                if (doMonthRICheck || forceMonthlyRICheck) {
                    if (element.getBudgetConstructionMonthly() != null
                            && !element.getBudgetConstructionMonthly().isEmpty()) {

                        BudgetConstructionMonthly budgetConstructionMonthly = element.getBudgetConstructionMonthly()
                                .get(0);
                        if (budgetConstructionMonthly != null) {
                            KualiInteger monthSum = KualiInteger.ZERO;
                            monthSum = monthSum
                                    .add(budgetConstructionMonthly.getFinancialDocumentMonth1LineAmount());
                            monthSum = monthSum
                                    .add(budgetConstructionMonthly.getFinancialDocumentMonth2LineAmount());
                            monthSum = monthSum
                                    .add(budgetConstructionMonthly.getFinancialDocumentMonth3LineAmount());
                            monthSum = monthSum
                                    .add(budgetConstructionMonthly.getFinancialDocumentMonth4LineAmount());
                            monthSum = monthSum
                                    .add(budgetConstructionMonthly.getFinancialDocumentMonth5LineAmount());
                            monthSum = monthSum
                                    .add(budgetConstructionMonthly.getFinancialDocumentMonth6LineAmount());
                            monthSum = monthSum
                                    .add(budgetConstructionMonthly.getFinancialDocumentMonth7LineAmount());
                            monthSum = monthSum
                                    .add(budgetConstructionMonthly.getFinancialDocumentMonth8LineAmount());
                            monthSum = monthSum
                                    .add(budgetConstructionMonthly.getFinancialDocumentMonth9LineAmount());
                            monthSum = monthSum
                                    .add(budgetConstructionMonthly.getFinancialDocumentMonth10LineAmount());
                            monthSum = monthSum
                                    .add(budgetConstructionMonthly.getFinancialDocumentMonth11LineAmount());
                            monthSum = monthSum
                                    .add(budgetConstructionMonthly.getFinancialDocumentMonth12LineAmount());

                            if (!monthSum.equals(element.getAccountLineAnnualBalanceAmount())) {
                                isValid &= false;
                                String pkeyVal = element.getFinancialObjectCode() + ","
                                        + element.getFinancialSubObjectCode();
                                GlobalVariables.getMessageMap().putError(
                                        KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT,
                                        BCKeyConstants.ERROR_MONTHLY_SUM_REQUEST_NOT_EQUAL, pkeyVal,
                                        monthSum.toString(),
                                        element.getAccountLineAnnualBalanceAmount().toString());
                            }
                        }
                    }
                }

                // check salary setting detail sum if expenditure line is a ss detail line
                // and salary setting option is turned on
                if (!SpringContext.getBean(SalarySettingService.class).isSalarySettingDisabled()) {
                    if (element.getLaborObject() != null) {
                        if (element.getLaborObject().isDetailPositionRequiredIndicator()) {

                            // sum the detail lines and compare against the accounting line request amount
                            KualiInteger salarySum = KualiInteger.ZERO;

                            // if salary setting detail exists, sum it otherwise default to zero
                            if (element.isPendingBudgetConstructionAppointmentFundingExists()) {

                                // run reportquery to get the salary request sum
                                salarySum = SpringContext.getBean(BudgetDocumentService.class)
                                        .getPendingBudgetConstructionAppointmentFundingRequestSum(element);

                            }

                            if (!salarySum.equals(element.getAccountLineAnnualBalanceAmount())) {
                                isValid &= false;
                                String pkeyVal = element.getFinancialObjectCode() + ","
                                        + element.getFinancialSubObjectCode();
                                GlobalVariables.getMessageMap().putError(
                                        KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT,
                                        BCKeyConstants.ERROR_SALARY_SUM_REQUEST_NOT_EQUAL, pkeyVal,
                                        salarySum.toString(),
                                        element.getAccountLineAnnualBalanceAmount().toString());
                            }
                        }
                    }
                }

                // only do benefits calc needed test if the user changed something - not if forcing the RI check
                if (isReqAmountValid && !element.getAccountLineAnnualBalanceAmount()
                        .equals(element.getPersistedAccountLineAnnualBalanceAmount())) {

                    // if benefits calculation is turned on,
                    // check if the line is benefits related and call for calculation after save
                    if (!SpringContext.getBean(BenefitsCalculationService.class).isBenefitsCalculationDisabled()) {

                        // retest for added errors since testing this line started - if none, test if benefits calc required
                        currentErrorCount = errors.getErrorCount();
                        isReqAmountValid = (currentErrorCount == originalErrorCount);

                        if (isReqAmountValid && element.getPositionObjectBenefit() != null
                                && !element.getPositionObjectBenefit().isEmpty()) {
                            budgetConstructionDocument.setBenefitsCalcNeeded(true);
                        }
                    }
                }
            }

            errors.removeFromErrorPath(linesErrorPath + "[" + index + "]");
        }

        return isValid;
    }

    /**
     * Checks a PBGL line. Assumes the line has been checked against the dd for formatting and if required
     *
     * @param budgetConstructionDocument
     * @param pendingBudgetConstructionGeneralLedger
     * @return
     */
    protected boolean checkPendingBudgetConstructionGeneralLedgerLine(
            BudgetConstructionDocument budgetConstructionDocument,
            PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger, MessageMap errors,
            boolean isRevenue, boolean isAdd) {
        LOG.debug("checkPendingBudgetConstructionGeneralLedgerLine() start");

        boolean isValid = true;

        // now make sure all the necessary business objects are fully populated
        // this refreshes any refs not done by populate for display purposes auto-update="none"
        pendingBudgetConstructionGeneralLedger.refreshNonUpdateableReferences();

        isValid &= validatePBGLLine(pendingBudgetConstructionGeneralLedger, isAdd);
        if (isValid) {

            // all lines must have objects defined with financialBudgetAggregation = 'O';
            isValid &= isBudgetAggregationAllowed(budgetAggregationCodesParamValues,
                    pendingBudgetConstructionGeneralLedger, errors, isAdd);

            isValid &= this.isBudgetAllowed(budgetConstructionDocument, KFSPropertyConstants.FINANCIAL_OBJECT_CODE,
                    errors, isAdd, false);

            // revenue specific checks
            if (isRevenue) {

                // no revenue lines in CnG accounts or SDCI
                isValid &= isNotSalarySettingOnly(salarySettingFundGroupsParamValues,
                        salarySettingSubFundGroupsParamValues, budgetConstructionDocument,
                        pendingBudgetConstructionGeneralLedger, errors, isRevenue, isAdd);

                // line must use matching revenue object type
                isValid &= isObjectTypeAllowed(revenueObjectTypesParamValues,
                        pendingBudgetConstructionGeneralLedger, errors, isRevenue, isAdd);

            } else {
                // expenditure specific checks

                // line must use matching expenditure object type
                isValid &= isObjectTypeAllowed(expenditureObjectTypesParamValues,
                        pendingBudgetConstructionGeneralLedger, errors, isRevenue, isAdd);

                // no lines using labor objects in non-wage accounts
                isValid &= isNonWagesAccountNotLaborObject(budgetConstructionDocument,
                        pendingBudgetConstructionGeneralLedger, errors, isAdd);

                // only lines using detail labor objects allowed in fund group CG and sfund group SDCI
                isValid &= isNotSalarySettingOnly(salarySettingFundGroupsParamValues,
                        salarySettingSubFundGroupsParamValues, budgetConstructionDocument,
                        pendingBudgetConstructionGeneralLedger, errors, isRevenue, isAdd);

                // no lines using fringe benefit target object codes allowed to be manually added by user
                // the lines are created by benefits calculation process
                isValid &= isNotFringeBenefitObject(fringeBenefitDesignatorCodesParamValues,
                        pendingBudgetConstructionGeneralLedger, errors, isAdd);
            }

        }

        if (!isValid) {
            LOG.info(
                    "business rule checks failed in checkPendingBudgetConstructionGeneralLedgerLine in BudgetConstructionRules");
        }

        LOG.debug("checkPendingBudgetConstructionGeneralLedgerLine() end");
        return isValid;
    }

    protected boolean validatePBGLLine(
            PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger, boolean isAdd) {
        if (pendingBudgetConstructionGeneralLedger == null) {
            throw new IllegalStateException(getKualiConfigurationService()
                    .getPropertyValueAsString(KFSKeyConstants.ERROR_DOCUMENT_NULL_ACCOUNTING_LINE));
        }

        // grab the service instance that will be needed by all the validate methods
        DataDictionary dd = dataDictionaryService.getDataDictionary();

        // retrieve each pbgl line object and validate
        boolean valid = true;

        // object code is required
        ObjectCode objectCode = pendingBudgetConstructionGeneralLedger.getFinancialObject();

        // this code calls a local version (not AccountingLineRuleHelper) of isValidObjectCode to add the bad value to the error
        // message
        if (isAdd) {
            valid &= isValidObjectCode(objectCode, pendingBudgetConstructionGeneralLedger.getFinancialObjectCode(),
                    dd, KFSConstants.FINANCIAL_OBJECT_CODE_PROPERTY_NAME);
        } else {
            valid &= isValidObjectCode(objectCode, pendingBudgetConstructionGeneralLedger.getFinancialObjectCode(),
                    dd, TARGET_ERROR_PROPERTY_NAME);
        }

        // sub object is not required
        if (StringUtils.isNotBlank(pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode())
                && !pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode()
                        .equalsIgnoreCase(KFSConstants.getDashFinancialSubObjectCode())) {
            SubObjectCode subObjectCode = pendingBudgetConstructionGeneralLedger.getFinancialSubObject();

            // this code calls a local version (not AccountingLineRuleHelper) of isValidSubObjectCode to add the bad value to the
            // error message
            if (isAdd) {
                valid &= isValidSubObjectCode(subObjectCode,
                        pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode(), dd,
                        KFSConstants.FINANCIAL_SUB_OBJECT_CODE_PROPERTY_NAME);
            } else {
                valid &= isValidSubObjectCode(subObjectCode,
                        pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode(), dd,
                        TARGET_ERROR_PROPERTY_NAME);
            }
        }

        return valid;
    }

    /**
     * Validates a single primitive in a BO
     *
     * @param object
     * @param attributeName
     * @param errorPrefix
     * @param validateRequired
     */
    protected void validatePrimitiveFromDescriptor(Object object, String attributeName, String errorPrefix,
            boolean validateRequired) {

        try {
            PropertyDescriptor attributeDescriptor = PropertyUtils.getPropertyDescriptor(object, attributeName);
            validatePrimitiveFromDescriptor(object.getClass().getName(), object, attributeDescriptor, "", true);
        } catch (NoSuchMethodException e) {
            throw new InfrastructureException(
                    "unable to find propertyDescriptor for property '" + attributeName + "'", e);
        } catch (IllegalAccessException e) {
            throw new InfrastructureException(
                    "unable to access propertyDescriptor for property '" + attributeName + "'", e);
        } catch (InvocationTargetException e) {
            throw new InfrastructureException("unable to invoke methods for property '" + attributeName + "'", e);
        }
    }

    /**
     * Validates a primitive in a BO
     *
     * @param entryName
     * @param object
     * @param propertyDescriptor
     * @param errorPrefix
     * @param validateRequired
     */
    protected void validatePrimitiveFromDescriptor(String entryName, Object object,
            PropertyDescriptor propertyDescriptor, String errorPrefix, boolean validateRequired) {

        // validate the primitive attributes if defined in the dictionary
        if (null != propertyDescriptor
                && dataDictionaryService.isAttributeDefined(entryName, propertyDescriptor.getName())) {
            Object value = ObjectUtils.getPropertyValue(object, propertyDescriptor.getName());
            Class propertyType = propertyDescriptor.getPropertyType();

            if (TypeUtils.isStringClass(propertyType) || TypeUtils.isIntegralClass(propertyType)
                    || TypeUtils.isDecimalClass(propertyType) || TypeUtils.isTemporalClass(propertyType)) {

                // check value format against dictionary
                if (value != null && StringUtils.isNotBlank(value.toString())) {
                    if (!TypeUtils.isTemporalClass(propertyType)) {
                        SpringContext.getBean(DictionaryValidationService.class).validate(object, entryName,
                                propertyDescriptor.getName(), false);
                    }
                } else if (validateRequired) {
                    SpringContext.getBean(DictionaryValidationService.class).validate(object, entryName,
                            propertyDescriptor.getName(), true);
                }
            }
        }
    }

    protected boolean isObjectTypeAllowed(Collection<String> paramValues,
            PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isRevenue,
            boolean isAdd) {
        boolean isAllowed = true;

        if (paramValues != null) {
            if (!paramValues.contains(accountingLine.getFinancialObject().getFinancialObjectTypeCode())) {
                isAllowed = false;

                String targetErrorProperty;
                if (isAdd) {
                    targetErrorProperty = KFSPropertyConstants.FINANCIAL_OBJECT_CODE;
                } else {
                    targetErrorProperty = TARGET_ERROR_PROPERTY_NAME;
                }

                if (isRevenue) {
                    this.putError(errors, targetErrorProperty,
                            BCKeyConstants.ERROR_BUDGET_OBJECT_TYPE_INVALID_REVENUE, isAdd,
                            accountingLine.getFinancialObjectCode(),
                            accountingLine.getFinancialObject().getFinancialObjectTypeCode());
                } else {
                    this.putError(errors, targetErrorProperty,
                            BCKeyConstants.ERROR_BUDGET_OBJECT_TYPE_INVALID_EXPENSE, isAdd,
                            accountingLine.getFinancialObjectCode(),
                            accountingLine.getFinancialObject().getFinancialObjectTypeCode());
                }
            }
        } else {
            isAllowed = false;
        }

        return isAllowed;
    }

    protected boolean isBudgetAggregationAllowed(Collection<String> paramValues,
            PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isAdd) {
        boolean isAllowed = true;

        if (paramValues != null) {
            if (!paramValues.contains(accountingLine.getFinancialObject().getFinancialBudgetAggregationCd())) {
                isAllowed = false;

                this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE,
                        KFSKeyConstants.ERROR_DOCUMENT_INCORRECT_OBJ_CODE_WITH_BUDGET_AGGREGATION, isAdd,
                        accountingLine.getFinancialObjectCode(),
                        accountingLine.getFinancialObject().getFinancialBudgetAggregationCd());
            }
        } else {
            isAllowed = false;
        }

        return isAllowed;
    }

    protected boolean isNewLineUnique(BudgetConstructionDocument budgetConstructionDocument,
            PendingBudgetConstructionGeneralLedger newLine, MessageMap errors, boolean isRevenue) {
        boolean isUnique = true;
        List<PendingBudgetConstructionGeneralLedger> existingLines;

        if (isRevenue) {
            existingLines = budgetConstructionDocument.getPendingBudgetConstructionGeneralLedgerRevenueLines();
        } else {
            existingLines = budgetConstructionDocument.getPendingBudgetConstructionGeneralLedgerExpenditureLines();
        }

        if (BudgetConstructionRuleUtil.hasExistingPBGLLine(existingLines, newLine)) {
            isUnique = false;
            errors.putError(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_BUDGET_LINE_EXISTS,
                    newLine.getFinancialObjectCode() + "," + newLine.getFinancialSubObjectCode());
        }

        return isUnique;
    }

    protected boolean isNonWagesAccountNotLaborObject(BudgetConstructionDocument budgetConstructionDocument,
            PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isAdd) {
        boolean isAllowed = true;

        if (budgetConstructionDocument.getAccount().getSubFundGroup() == null
                || !budgetConstructionDocument.getAccount().getSubFundGroup().isSubFundGroupWagesIndicator()) {
            if (accountingLine.getLaborObject() != null) {
                isAllowed = false;
                this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE,
                        BCKeyConstants.ERROR_LABOR_OBJECT_IN_NOWAGES_ACCOUNT, isAdd,
                        accountingLine.getFinancialObjectCode());
            }
        }
        return isAllowed;
    }

    protected boolean isNotFringeBenefitObject(Collection<String> paramValues,
            PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isAdd) {
        boolean isAllowed = true;

        if (paramValues != null) {
            if (accountingLine.getLaborObject() != null) {
                if (paramValues.contains(accountingLine.getLaborObject().getFinancialObjectFringeOrSalaryCode())) {
                    isAllowed = false;
                    this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE,
                            BCKeyConstants.ERROR_FRINGE_BENEFIT_OBJECT_NOT_ALLOWED, isAdd,
                            accountingLine.getFinancialObjectCode());
                }
            }
        } else {
            isAllowed = false;
        }

        return isAllowed;
    }

    protected boolean isNotSalarySettingOnly(Collection<String> fundGroupParamValues,
            Collection<String> subfundGroupParamValues, BudgetConstructionDocument budgetConstructionDocument,
            PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isRevenue,
            boolean isAdd) {
        boolean isAllowed = true;

        // check if account belongs to a fund or subfund that only allows salary setting lines
        AccountSalarySettingOnlyCause retVal = budgetParameterService
                .isSalarySettingOnlyAccount(budgetConstructionDocument);
        if (retVal != AccountSalarySettingOnlyCause.MISSING_PARAM) {
            if (retVal != AccountSalarySettingOnlyCause.NONE) {

                // the line must use an object that is a detail salary labor object
                if (isRevenue || accountingLine.getLaborObject() == null
                        || !accountingLine.getLaborObject().isDetailPositionRequiredIndicator()) {

                    isAllowed = false;
                    if (retVal == AccountSalarySettingOnlyCause.FUND
                            || retVal == AccountSalarySettingOnlyCause.FUND_AND_SUBFUND) {
                        this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE,
                                BCKeyConstants.ERROR_SALARY_SETTING_OBJECT_ONLY, isAdd,
                                "fund " + budgetConstructionDocument.getAccount().getSubFundGroup()
                                        .getFundGroupCode());

                    }
                    if (retVal == AccountSalarySettingOnlyCause.SUBFUND
                            || retVal == AccountSalarySettingOnlyCause.FUND_AND_SUBFUND) {
                        this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE,
                                BCKeyConstants.ERROR_SALARY_SETTING_OBJECT_ONLY, isAdd,
                                "subfund " + budgetConstructionDocument.getAccount().getSubFundGroup()
                                        .getSubFundGroupCode());
                    }
                }
            }

        } else {
            // missing system parameter
            this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE,
                    BCKeyConstants.ERROR_SALARY_SETTING_OBJECT_ONLY_NO_PARAMETER, isAdd,
                    budgetConstructionDocument.getAccount().getSubFundGroup().getFundGroupCode() + ","
                            + budgetConstructionDocument.getAccount().getSubFundGroup().getSubFundGroupCode());
            isAllowed = false;
        }

        return isAllowed;
    }

    /**
     * runs rule checks that don't allow a budget
     *
     * @param budgetConstructionDocument
     * @param propertyName
     * @param errors
     * @param isAdd
     * @param isDocumentAdd
     * @return
     */
    protected boolean isBudgetAllowed(BudgetConstructionDocument budgetConstructionDocument, String propertyName,
            MessageMap errors, boolean isAdd, boolean isDocumentAdd) {
        boolean isAllowed = true;
        SimpleDateFormat tdf = new SimpleDateFormat("MM/dd/yyyy hh:mm a");

        // is account closed?
        if (!budgetConstructionDocument.getAccount().isActive()) {
            isAllowed = false;
            this.putError(errors, propertyName, KFSKeyConstants.ERROR_CLOSED, isAdd,
                    "account: " + budgetConstructionDocument.getAccountNumber());
        }

        // is account expiration no budget allowed, currently < 1/1/(byfy-2)?
        Calendar expDate = BudgetConstructionRuleUtil
                .getNoBudgetAllowedExpireDate(budgetConstructionDocument.getUniversityFiscalYear());
        if (budgetConstructionDocument.getAccount().isExpired(expDate)) {
            isAllowed = false;
            this.putError(errors, propertyName, BCKeyConstants.ERROR_NO_BUDGET_ALLOWED, isAdd,
                    budgetConstructionDocument.getAccountNumber(),
                    tdf.format(budgetConstructionDocument.getAccount().getAccountExpirationDate()));

        }

        // is account a cash control account
        if (budgetConstructionDocument.getAccount().getBudgetRecordingLevelCode()
                .equalsIgnoreCase(BCConstants.BUDGET_RECORDING_LEVEL_N)) {
            isAllowed = false;
            this.putError(errors, propertyName, BCKeyConstants.ERROR_BUDGET_RECORDING_LEVEL_NOT_ALLOWED, isAdd,
                    budgetConstructionDocument.getAccountNumber(), BCConstants.BUDGET_RECORDING_LEVEL_N);
        }

        // grab the service instance that will be needed by all the validate methods
        DataDictionary dd = dataDictionaryService.getDataDictionary();

        if (StringUtils.isNotBlank(budgetConstructionDocument.getSubAccountNumber()) && !budgetConstructionDocument
                .getSubAccountNumber().equalsIgnoreCase(KFSConstants.getDashSubAccountNumber())) {
            SubAccount subAccount = budgetConstructionDocument.getSubAccount();

            // is subacct inactive or not exist?
            // this code calls a local version (not AccountingLineRuleHelper) of isValidSubAccount
            // to add the bad value to the error message
            if (isAdd) {
                if (isDocumentAdd) {
                    isAllowed &= this.isValidSubAccount(subAccount,
                            budgetConstructionDocument.getSubAccountNumber(), dd,
                            KFSPropertyConstants.SUB_ACCOUNT_NUMBER);
                } else {
                    isAllowed &= this.isValidSubAccount(subAccount,
                            budgetConstructionDocument.getSubAccountNumber(), dd, propertyName);
                }
            } else {
                isAllowed &= this.isValidSubAccount(subAccount, budgetConstructionDocument.getSubAccountNumber(),
                        dd, TARGET_ERROR_PROPERTY_NAME);
            }

            // is subacct type cost share?
            // this hack is here since kuldev is missing one to one instances
            // and the RI ojb mapping produces an error when attempting to test if the
            // A21SubAccount attached to the document's SubAccount is null
            Map<String, Object> searchCriteria = new HashMap<String, Object>();
            searchCriteria.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE,
                    budgetConstructionDocument.getChartOfAccountsCode());
            searchCriteria.put(KFSPropertyConstants.ACCOUNT_NUMBER, budgetConstructionDocument.getAccountNumber());
            searchCriteria.put(KFSPropertyConstants.SUB_ACCOUNT_NUMBER,
                    budgetConstructionDocument.getSubAccountNumber());
            A21SubAccount a21SubAccount = businessObjectService.findByPrimaryKey(A21SubAccount.class,
                    searchCriteria);
            if (ObjectUtils.isNotNull(a21SubAccount)) {
                if (a21SubAccount.getSubAccountTypeCode()
                        .equalsIgnoreCase(KFSConstants.SubAccountType.COST_SHARE)) {
                    isAllowed = false;
                    this.putError(errors, KFSPropertyConstants.SUB_ACCOUNT_NUMBER,
                            BCKeyConstants.ERROR_SUB_ACCOUNT_TYPE_NOT_ALLOWED, isAdd,
                            budgetConstructionDocument.getSubAccountNumber(),
                            KFSConstants.SubAccountType.COST_SHARE);
                }
            }
        }

        return isAllowed;
    }

    public boolean isValidAccount(Account account, String value, DataDictionary dataDictionary,
            String errorPropertyName) {
        String label = dataDictionary.getBusinessObjectEntry(Account.class.getName())
                .getAttributeDefinition(KFSConstants.ACCOUNT_NUMBER_PROPERTY_NAME).getShortLabel();

        // make sure it exists
        if (ObjectUtils.isNull(account)) {
            GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_EXISTENCE,
                    label + ":" + value);
            return false;
        }

        return true;
    }

    public boolean isValidSubAccount(SubAccount subAccount, String value, DataDictionary dataDictionary,
            String errorPropertyName) {
        String label = dataDictionary.getBusinessObjectEntry(SubAccount.class.getName())
                .getAttributeDefinition(KFSConstants.SUB_ACCOUNT_NUMBER_PROPERTY_NAME).getShortLabel();

        // make sure it exists
        if (ObjectUtils.isNull(subAccount)) {
            GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_EXISTENCE,
                    label + ":" + value);
            return false;
        }

        // check to make sure it is active
        if (!subAccount.isActive()) {
            GlobalVariables.getMessageMap().putError(errorPropertyName,
                    KFSKeyConstants.ERROR_DOCUMENT_SUB_ACCOUNT_INACTIVE, label + ":" + value);
            return false;
        }

        return true;
    }

    /**
     * Runs existence and active tests on the SubObjectCode reference This method is differenct than the one in
     * AccountingLineRuleHelper in that it adds the bad value to the errormessage This method signature should probably be added to
     * AccountingLineRuleHelper
     *
     * @param subObjectCode
     * @param value
     * @param dataDictionary
     * @param errorPropertyName
     * @return
     */
    public boolean isValidSubObjectCode(SubObjectCode subObjectCode, String value, DataDictionary dataDictionary,
            String errorPropertyName) {
        String label = dataDictionary.getBusinessObjectEntry(SubObjectCode.class.getName())
                .getAttributeDefinition(KFSConstants.FINANCIAL_SUB_OBJECT_CODE_PROPERTY_NAME).getShortLabel();

        // make sure it exists
        if (ObjectUtils.isNull(subObjectCode)) {
            GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_EXISTENCE,
                    label + ":" + value);
            return false;
        }

        // check active flag
        if (!subObjectCode.isActive()) {
            GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_INACTIVE,
                    label + ":" + value);
            return false;
        }
        return true;
    }

    /**
     * Runs existence and active tests on the ObjectCode reference This method is differenct than the one in
     * AccountingLineRuleHelper in that it adds the bad value to the errormessage This method signature should probably be added to
     * AccountingLineRuleHelper
     *
     * @param objectCode
     * @param value
     * @param dataDictionary
     * @param errorPropertyName
     * @return
     */
    public boolean isValidObjectCode(ObjectCode objectCode, String value, DataDictionary dataDictionary,
            String errorPropertyName) {
        String label = dataDictionary.getBusinessObjectEntry(ObjectCode.class.getName())
                .getAttributeDefinition(KFSConstants.FINANCIAL_OBJECT_CODE_PROPERTY_NAME).getShortLabel();

        // make sure it exists
        if (ObjectUtils.isNull(objectCode)) {
            GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_EXISTENCE,
                    label + ":" + value);
            return false;
        }

        // check active status
        if (!objectCode.isFinancialObjectActiveCode()) {
            GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_INACTIVE,
                    label + ":" + value);
            return false;
        }

        return true;
    }

    /**
     * puts error to errormap for propertyName if isAdd, otherwise the property name is replaced with value of
     * TARGET_ERROR_PROPERTY_NAME
     *
     * @param propertyName
     * @param errorKey
     * @param isAdd
     * @param errorParameters
     */
    protected void putError(MessageMap errors, String propertyName, String errorKey, boolean isAdd,
            String... errorParameters) {

        if (isAdd) {
            errors.putError(propertyName, errorKey, errorParameters);
        } else {
            errors.putError(TARGET_ERROR_PROPERTY_NAME, errorKey, errorParameters);
        }

    }
}