001    /*
002     * Copyright 2011 The Kuali Foundation.
003     * 
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     * 
008     * http://www.opensource.org/licenses/ecl2.php
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.kfs.module.bc.document.validation.impl;
017    
018    import java.beans.PropertyDescriptor;
019    import java.lang.reflect.InvocationTargetException;
020    import java.text.SimpleDateFormat;
021    import java.util.Arrays;
022    import java.util.Calendar;
023    import java.util.Collections;
024    import java.util.HashMap;
025    import java.util.Iterator;
026    import java.util.List;
027    import java.util.Map;
028    
029    import org.apache.commons.beanutils.PropertyUtils;
030    import org.apache.commons.lang.StringUtils;
031    import org.kuali.kfs.coa.businessobject.A21SubAccount;
032    import org.kuali.kfs.coa.businessobject.Account;
033    import org.kuali.kfs.coa.businessobject.ObjectCode;
034    import org.kuali.kfs.coa.businessobject.SubAccount;
035    import org.kuali.kfs.coa.businessobject.SubObjectCode;
036    import org.kuali.kfs.fp.service.FiscalYearFunctionControlService;
037    import org.kuali.kfs.module.bc.BCConstants;
038    import org.kuali.kfs.module.bc.BCKeyConstants;
039    import org.kuali.kfs.module.bc.BCPropertyConstants;
040    import org.kuali.kfs.module.bc.BCConstants.AccountSalarySettingOnlyCause;
041    import org.kuali.kfs.module.bc.BCConstants.MonthSpreadDeleteType;
042    import org.kuali.kfs.module.bc.businessobject.BudgetConstructionMonthly;
043    import org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionGeneralLedger;
044    import org.kuali.kfs.module.bc.document.BudgetConstructionDocument;
045    import org.kuali.kfs.module.bc.document.service.BenefitsCalculationService;
046    import org.kuali.kfs.module.bc.document.service.BudgetDocumentService;
047    import org.kuali.kfs.module.bc.document.service.BudgetParameterService;
048    import org.kuali.kfs.module.bc.document.service.SalarySettingService;
049    import org.kuali.kfs.module.bc.document.validation.AddBudgetConstructionDocumentRule;
050    import org.kuali.kfs.module.bc.document.validation.AddPendingBudgetGeneralLedgerLineRule;
051    import org.kuali.kfs.module.bc.document.validation.DeleteMonthlySpreadRule;
052    import org.kuali.kfs.module.bc.document.validation.DeletePendingBudgetGeneralLedgerLineRule;
053    import org.kuali.kfs.module.bc.document.validation.SaveMonthlyBudgetRule;
054    import org.kuali.kfs.module.bc.util.BudgetParameterFinder;
055    import org.kuali.kfs.sys.KFSConstants;
056    import org.kuali.kfs.sys.KFSKeyConstants;
057    import org.kuali.kfs.sys.KFSPropertyConstants;
058    import org.kuali.kfs.sys.context.SpringContext;
059    import org.kuali.kfs.sys.document.service.AccountingLineRuleHelperService;
060    import org.kuali.rice.kns.datadictionary.DataDictionary;
061    import org.kuali.rice.kns.document.Document;
062    import org.kuali.rice.kns.exception.InfrastructureException;
063    import org.kuali.rice.kns.rules.TransactionalDocumentRuleBase;
064    import org.kuali.rice.kns.service.BusinessObjectService;
065    import org.kuali.rice.kns.service.DataDictionaryService;
066    import org.kuali.rice.kns.service.PersistenceService;
067    import org.kuali.rice.kns.util.ErrorMap;
068    import org.kuali.rice.kns.util.GlobalVariables;
069    import org.kuali.rice.kns.util.KNSConstants;
070    import org.kuali.rice.kns.util.KualiInteger;
071    import org.kuali.rice.kns.util.MessageMap;
072    import org.kuali.rice.kns.util.ObjectUtils;
073    import org.kuali.rice.kns.util.TypeUtils;
074    
075    public class BudgetConstructionDocumentRules extends TransactionalDocumentRuleBase implements AddBudgetConstructionDocumentRule<BudgetConstructionDocument>, AddPendingBudgetGeneralLedgerLineRule<BudgetConstructionDocument, PendingBudgetConstructionGeneralLedger>, DeletePendingBudgetGeneralLedgerLineRule<BudgetConstructionDocument, PendingBudgetConstructionGeneralLedger>, DeleteMonthlySpreadRule<BudgetConstructionDocument>, SaveMonthlyBudgetRule<BudgetConstructionDocument, BudgetConstructionMonthly> {
076        protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BudgetConstructionDocumentRules.class);
077    
078        // some services used here - other service refs are from parent classes
079        // if this class is extended we may need to create protected getters
080        protected static BudgetParameterService budgetParameterService = SpringContext.getBean(BudgetParameterService.class);
081        protected static AccountingLineRuleHelperService accountingLineRuleHelper = SpringContext.getBean(AccountingLineRuleHelperService.class);
082        protected static DataDictionaryService dataDictionaryService = SpringContext.getBean(DataDictionaryService.class);
083        protected static SalarySettingService salarySettingService = SpringContext.getBean(SalarySettingService.class);
084        protected static BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class);
085        protected static FiscalYearFunctionControlService fiscalYearFunctionControlService = SpringContext.getBean(FiscalYearFunctionControlService.class);
086    
087        protected List<String> revenueObjectTypesParamValues = BudgetParameterFinder.getRevenueObjectTypes();
088        protected List<String> expenditureObjectTypesParamValues = BudgetParameterFinder.getExpenditureObjectTypes();
089        protected List<String> budgetAggregationCodesParamValues = BudgetParameterFinder.getBudgetAggregationCodes();
090        protected List<String> fringeBenefitDesignatorCodesParamValues = BudgetParameterFinder.getFringeBenefitDesignatorCodes();
091        protected List<String> salarySettingFundGroupsParamValues = BudgetParameterFinder.getSalarySettingFundGroups();
092        protected List<String> salarySettingSubFundGroupsParamValues = BudgetParameterFinder.getSalarySettingSubFundGroups();
093    
094        // this field is highlighted for any errors found on an existing line
095        protected static final String TARGET_ERROR_PROPERTY_NAME = KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT;
096    
097        public BudgetConstructionDocumentRules() {
098            super();
099        }
100    
101        /**
102         * @see org.kuali.kfs.module.bc.document.validation.AddBudgetConstructionDocumentRule#processAddBudgetConstructionDocumentRules(org.kuali.kfs.module.bc.document.BudgetConstructionDocument)
103         */
104        public boolean processAddBudgetConstructionDocumentRules(BudgetConstructionDocument budgetConstructionDocument) {
105            LOG.debug("processAddBudgetConstructionDocumentRules(Document) - start");
106    
107            MessageMap errors = GlobalVariables.getMessageMap();
108            boolean isValid = true;
109    
110            // validate primitives for required field and formatting checks
111            int originalErrorCount = errors.getErrorCount();
112            getDictionaryValidationService().validateBusinessObject(budgetConstructionDocument);
113    
114            // check to see if any errors were reported
115            int currentErrorCount = errors.getErrorCount();
116            isValid &= (currentErrorCount == originalErrorCount);
117            if (!isValid) {
118                return isValid;
119            }
120    
121            // can't create BC documents when in system view only mode
122            // let the user know this up front
123            if (!fiscalYearFunctionControlService.isBudgetUpdateAllowed(budgetConstructionDocument.getUniversityFiscalYear())) {
124                errors.putError(KFSPropertyConstants.ACCOUNT_NUMBER, BCKeyConstants.MESSAGE_BUDGET_SYSTEM_VIEW_ONLY);
125                isValid &= false;
126            }
127    
128            // check existence of account first
129            DataDictionary dd = dataDictionaryService.getDataDictionary();
130            String pkeyValue = budgetConstructionDocument.getChartOfAccountsCode() + "-" + budgetConstructionDocument.getAccountNumber();
131            isValid &= isValidAccount(budgetConstructionDocument.getAccount(), pkeyValue, dd, KFSPropertyConstants.ACCOUNT_NUMBER);
132            if (isValid) {
133    
134                // run the rules checks preventing BC document creation - assumes account exists
135                isValid &= this.isBudgetAllowed(budgetConstructionDocument, KFSPropertyConstants.ACCOUNT_NUMBER, errors, true, true);
136            }
137    
138            if (!isValid) {
139    
140                // tell the user we can't create a new BC document along with the error reasons
141                GlobalVariables.getMessageList().add(BCKeyConstants.MESSAGE_BUDGET_NOCREATE_DOCUMENT);
142            }
143    
144            LOG.debug("processAddBudgetConstructionDocumentRules(Document) - end");
145            return isValid;
146        }
147    
148        /**
149         * Runs business rules prior to saving Budget Document proper. This is different than saving typical KFS documents in that the
150         * document is not saved to the user's inbox. Saved Budget Documents must meet the same state requirements as the typical KFS
151         * routed document, so required field checks must be done. Budget Documents can be opened by a user in edit mode multiple times
152         * and while in edit mode documents can be pushed down the review hierarchy, monthly budgets and appointment funding updated,
153         * benefits calculated, etc. Each of these operations require the document's data be in a consistent state with respect to
154         * business rules before the operation be performed.
155         * 
156         * @see org.kuali.rice.kns.rules.DocumentRuleBase#processSaveDocument(org.kuali.rice.kns.document.Document)
157         */
158        @Override
159        public boolean processSaveDocument(Document document) {
160            LOG.debug("processSaveDocument(Document) - start");
161    
162            boolean isValid = true;
163    
164            // run through the attributes recursively and check dd stuff
165            isValid &= isDocumentAttributesValid(document, true);
166    
167            if (isValid) {
168                isValid &= processSaveBudgetDocumentRules((BudgetConstructionDocument) document, MonthSpreadDeleteType.NONE);
169            }
170    
171            // no custom save rules since we are overriding and doing what we want here already
172    
173            LOG.debug("processSaveDocument(Document) - end");
174            return isValid;
175        }
176    
177        public boolean processDeleteMonthlySpreadRules(BudgetConstructionDocument budgetConstructionDocument, MonthSpreadDeleteType monthSpreadDeleteType) {
178            LOG.debug("processDeleteRevenueMonthlySpreadRules(Document) - start");
179    
180            boolean isValid = true;
181    
182            // run through the attributes recursively and check dd stuff
183            isValid &= isDocumentAttributesValid(budgetConstructionDocument, true);
184    
185            if (isValid) {
186                isValid &= processSaveBudgetDocumentRules(budgetConstructionDocument, monthSpreadDeleteType);
187            }
188    
189            // no custom save rules since we are overriding and doing what we want here already
190    
191            LOG.debug("processDeleteRevenueMonthlySpreadRules(Document) - end");
192            return isValid;
193    
194        }
195    
196        /**
197         * Iterates through existing revenue and expenditure lines to do validation, ri checks on object/sub-object code and request
198         * amount referential integrity checks against appointment funding and monthly detail amounts. Checks are performed when the
199         * request amount has been updated, since initial add action, the last save event or since opening the document, whatever is
200         * latest.
201         * 
202         * @see org.kuali.module.budget.rule.SaveBudgetDocumentRule#processSaveBudgetDocumentRules(D)
203         */
204        public boolean processSaveBudgetDocumentRules(BudgetConstructionDocument budgetConstructionDocument, MonthSpreadDeleteType monthSpreadDeleteType) {
205    
206            MessageMap errors = GlobalVariables.getMessageMap();
207            boolean doRevMonthRICheck = true;
208            boolean doExpMonthRICheck = true;
209            boolean isValid = true;
210            int originalErrorCount;
211            int currentErrorCount;
212    
213            // refresh only the doc refs we need
214            List refreshFields = Collections.unmodifiableList(Arrays.asList(new String[] { KFSPropertyConstants.ACCOUNT, KFSPropertyConstants.SUB_ACCOUNT }));
215            SpringContext.getBean(PersistenceService.class).retrieveReferenceObjects(budgetConstructionDocument, refreshFields);
216    
217            errors.addToErrorPath(KNSConstants.DOCUMENT_PROPERTY_NAME);
218    
219            if (monthSpreadDeleteType == MonthSpreadDeleteType.REVENUE) {
220                doRevMonthRICheck = false;
221                doExpMonthRICheck = true;
222            }
223            else {
224                if (monthSpreadDeleteType == MonthSpreadDeleteType.EXPENDITURE) {
225                    doRevMonthRICheck = true;
226                    doExpMonthRICheck = false;
227                }
228            }
229    
230            // iterate and validate revenue lines
231            isValid &= this.checkPendingBudgetConstructionGeneralLedgerLines(budgetConstructionDocument, errors, true, doRevMonthRICheck);
232    
233            // iterate and validate expenditure lines
234            isValid &= this.checkPendingBudgetConstructionGeneralLedgerLines(budgetConstructionDocument, errors, false, doExpMonthRICheck);
235    
236            errors.removeFromErrorPath(KNSConstants.DOCUMENT_PROPERTY_NAME);
237    
238            return isValid;
239        }
240    
241        /**
242         * Checks a new PBGL line. Comprehensive checks are done.
243         * 
244         * @param budgetConstructionDocument
245         * @param pendingBudgetConstructionGeneralLedger
246         * @param isRevenue
247         * @return
248         */
249        public boolean processAddPendingBudgetGeneralLedgerLineRules(BudgetConstructionDocument budgetConstructionDocument, PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger, boolean isRevenue) {
250            LOG.debug("processAddPendingBudgetGeneralLedgerLineRules() start");
251    
252            // List refreshFields;
253            MessageMap errors = GlobalVariables.getMessageMap();
254            boolean isValid = true;
255    
256            int originalErrorCount = errors.getErrorCount();
257    
258            // validate primitives for required field and formatting checks
259            getDictionaryValidationService().validateBusinessObject(pendingBudgetConstructionGeneralLedger);
260    
261            // check to see if any errors were reported
262            int currentErrorCount = errors.getErrorCount();
263            isValid &= (currentErrorCount == originalErrorCount);
264    
265            if (isValid) {
266    
267                // refresh only the doc refs we need
268                List refreshFields = Collections.unmodifiableList(Arrays.asList(new String[] { KFSPropertyConstants.ACCOUNT, KFSPropertyConstants.SUB_ACCOUNT }));
269                SpringContext.getBean(PersistenceService.class).retrieveReferenceObjects(budgetConstructionDocument, refreshFields);
270                // budgetConstructionDocument.getSubAccount().refreshReferenceObject(KFSPropertyConstants.A21_SUB_ACCOUNT);
271    
272                isValid &= this.checkPendingBudgetConstructionGeneralLedgerLine(budgetConstructionDocument, pendingBudgetConstructionGeneralLedger, errors, isRevenue, true);
273    
274    
275                if (isValid) {
276                    // line checks ok - does line already exist in target revenue or expenditure list
277                    isValid &= isNewLineUnique(budgetConstructionDocument, pendingBudgetConstructionGeneralLedger, errors, isRevenue);
278                }
279            }
280    
281            if (!isValid) {
282                LOG.info("business rule checks failed in processAddPendingBudgetGeneralLedgerLineRules in BudgetConstructionRules");
283            }
284    
285            LOG.debug("processAddPendingBudgetGeneralLedgerLineRules() end");
286            return isValid;
287        }
288    
289        /**
290         * Runs rules for deleting an existing revenue or expenditure line.
291         * 
292         * @param budgetConstructionDocument
293         * @param pendingBudgetConstructionGeneralLedger
294         * @param isRevenue
295         * @return
296         */
297        public boolean processDeletePendingBudgetGeneralLedgerLineRules(BudgetConstructionDocument budgetConstructionDocument, PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger, boolean isRevenue) {
298            LOG.debug("processDeletePendingBudgetGeneralLedgerLineRules() start");
299    
300            MessageMap errors = GlobalVariables.getMessageMap();
301            boolean isValid = true;
302    
303            // no delete allowed if base exists, the delete button shouldn't even exist in this case, but checking anyway
304            if (pendingBudgetConstructionGeneralLedger.getFinancialBeginningBalanceLineAmount().isZero()) {
305                isValid &= true;
306            }
307            else {
308                isValid &= false;
309                String pkeyVal = pendingBudgetConstructionGeneralLedger.getFinancialObjectCode() + "," + pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode();
310                GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT, BCKeyConstants.ERROR_NO_DELETE_ALLOWED_WITH_BASE, pkeyVal);
311            }
312    
313            if (!isRevenue) {
314                // no lines using fringe benefit target object codes allowed to be manually deleted by user
315                // the lines are created by benefits calculation process
316                // again the delete button shouldn't even exist
317                isValid &= isNotFringeBenefitObject(fringeBenefitDesignatorCodesParamValues, pendingBudgetConstructionGeneralLedger, errors, false);
318    
319                // no deletion if salary setting option is turned on
320                // and the line is a salary detail line and detail recs exist
321                if (!SpringContext.getBean(SalarySettingService.class).isSalarySettingDisabled()) {
322                    if (pendingBudgetConstructionGeneralLedger.getLaborObject() != null) {
323                        if (pendingBudgetConstructionGeneralLedger.getLaborObject().isDetailPositionRequiredIndicator()) {
324                            if (pendingBudgetConstructionGeneralLedger.isPendingBudgetConstructionAppointmentFundingExists()) {
325                                isValid &= false;
326                                String pkeyVal = pendingBudgetConstructionGeneralLedger.getFinancialObjectCode() + "," + pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode();
327                                GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT, BCKeyConstants.ERROR_NO_DELETE_ALLOWED_SALARY_DETAIL, pkeyVal);
328                            }
329                        }
330                    }
331                }
332                if (!SpringContext.getBean(BenefitsCalculationService.class).isBenefitsCalculationDisabled()) {
333    
334                    // benefits calc is turned on, if the line is valid to remove and the request is not zero, set to calc benefits
335                    if (isValid && pendingBudgetConstructionGeneralLedger.getPositionObjectBenefit() != null && !pendingBudgetConstructionGeneralLedger.getPositionObjectBenefit().isEmpty()) {
336                        budgetConstructionDocument.setBenefitsCalcNeeded(true);
337    
338                        // test if the line has monthly budgets
339                        // this assumes business rule of non-zero monthly budget not allowed to sum to a zero annual amount
340                        // that is, if annual amount is zero, the monthly record contains all zeros
341                        if (pendingBudgetConstructionGeneralLedger.getBudgetConstructionMonthly() != null && !pendingBudgetConstructionGeneralLedger.getBudgetConstructionMonthly().isEmpty()) {
342                            budgetConstructionDocument.setMonthlyBenefitsCalcNeeded(true);
343                        }
344                    }
345                }
346            }
347    
348            LOG.debug("processDeletePendingBudgetGeneralLedgerLineRules() end");
349            return isValid;
350        }
351    
352        /**
353         * @see org.kuali.kfs.module.bc.document.validation.SaveMonthlyBudgetRule#processSaveMonthlyBudgetRules(org.kuali.kfs.module.bc.document.BudgetConstructionDocument,
354         *      org.kuali.kfs.module.bc.businessobject.BudgetConstructionMonthly)
355         */
356        public boolean processSaveMonthlyBudgetRules(BudgetConstructionDocument budgetConstructionDocument, BudgetConstructionMonthly budgetConstructionMonthly) {
357            LOG.debug("processSaveMonthlyBudgetRules() start");
358    
359            budgetConstructionMonthly.refreshReferenceObject("pendingBudgetConstructionGeneralLedger");
360            PendingBudgetConstructionGeneralLedger pbgl = budgetConstructionMonthly.getPendingBudgetConstructionGeneralLedger();
361            MessageMap errors = GlobalVariables.getMessageMap();
362            boolean isValid = true;
363    
364            int originalErrorCount = errors.getErrorCount();
365    
366            // validate primitives for required field and formatting checks
367            getDictionaryValidationService().validateBusinessObject(budgetConstructionMonthly);
368    
369            // check to see if any errors were reported
370            int currentErrorCount = errors.getErrorCount();
371            isValid &= (currentErrorCount == originalErrorCount);
372    
373            // Check special cleanup mode case and berate user on save of anything.
374            // The user should delete the row, which bypasses this rule.
375            if (!budgetConstructionDocument.isBudgetableDocument()) {
376                isValid &= Boolean.FALSE;
377                errors.putError(BCPropertyConstants.FINANCIAL_DOCUMENT_MONTH1_LINE_AMOUNT, BCKeyConstants.ERROR_BUDGET_DOCUMENT_NOT_BUDGETABLE, budgetConstructionDocument.getAccountNumber() + ";" + budgetConstructionDocument.getSubAccountNumber());
378            }
379    
380            if (isValid) {
381                KualiInteger monthlyTotal = budgetConstructionMonthly.getFinancialDocumentMonthTotalLineAmount();
382                if (!salarySettingService.isSalarySettingDisabled()) {
383                    if (pbgl.getLaborObject() != null && pbgl.getLaborObject().isDetailPositionRequiredIndicator()) {
384    
385                        // no request amount overrides allowed for salary setting detail lines
386                        if (!monthlyTotal.equals(pbgl.getAccountLineAnnualBalanceAmount())) {
387                            isValid &= false;
388                            errors.putError(BCPropertyConstants.FINANCIAL_DOCUMENT_MONTH1_LINE_AMOUNT, BCKeyConstants.ERROR_MONTHLY_DETAIL_SALARY_OVERIDE, budgetConstructionMonthly.getFinancialObjectCode(), monthlyTotal.toString(), pbgl.getAccountLineAnnualBalanceAmount().toString());
389                        }
390                    }
391                }
392    
393                // check for monthly total adding to zero (makes no sense)
394                if (monthlyTotal.isZero()) {
395                    boolean nonZeroMonthlyExists = false;
396                    nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth1LineAmount().isNonZero();
397                    nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth2LineAmount().isNonZero();
398                    nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth3LineAmount().isNonZero();
399                    nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth4LineAmount().isNonZero();
400                    nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth5LineAmount().isNonZero();
401                    nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth6LineAmount().isNonZero();
402                    nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth7LineAmount().isNonZero();
403                    nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth8LineAmount().isNonZero();
404                    nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth9LineAmount().isNonZero();
405                    nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth10LineAmount().isNonZero();
406                    nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth11LineAmount().isNonZero();
407                    nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth12LineAmount().isNonZero();
408                    if (nonZeroMonthlyExists) {
409                        isValid &= false;
410                        errors.putError(BCPropertyConstants.FINANCIAL_DOCUMENT_MONTH1_LINE_AMOUNT, BCKeyConstants.ERROR_MONTHLY_TOTAL_ZERO);
411                    }
412                }
413            }
414            else {
415                LOG.info("business rule checks failed in processSaveMonthlyBudgetRules in BudgetConstructionDocumentRules");
416            }
417    
418            LOG.debug("processSaveMonthlyBudgetRules() end");
419            return isValid;
420        }
421    
422        /**
423         * Iterates existing revenue or expenditure lines. Checks if request amount is non-zero or has changed and runs business rules
424         * on the line.
425         * 
426         * @param budgetConstructionDocument
427         * @param errors
428         * @param isRevenue
429         * @return
430         */
431        protected boolean checkPendingBudgetConstructionGeneralLedgerLines(BudgetConstructionDocument budgetConstructionDocument, MessageMap errors, boolean isRevenue, boolean doMonthRICheck) {
432    
433            boolean isValid = true;
434            boolean isReqAmountValid;
435            int originalErrorCount;
436            int currentErrorCount;
437            List<PendingBudgetConstructionGeneralLedger> pendingBudgetConstructionGeneralLedgerLines;
438            String linesErrorPath;
439    
440    
441            if (isRevenue) {
442                pendingBudgetConstructionGeneralLedgerLines = budgetConstructionDocument.getPendingBudgetConstructionGeneralLedgerRevenueLines();
443                linesErrorPath = BCPropertyConstants.PENDING_BUDGET_CONSTRUCTION_GENERAL_LEDGER_REVENUE_LINES;
444            }
445            else {
446                pendingBudgetConstructionGeneralLedgerLines = budgetConstructionDocument.getPendingBudgetConstructionGeneralLedgerExpenditureLines();
447                linesErrorPath = BCPropertyConstants.PENDING_BUDGET_CONSTRUCTION_GENERAL_LEDGER_EXPENDITURE_LINES;
448            }
449    
450            // iterate revenue or expenditure lines
451            Integer index = 0;
452            for (Iterator iter = pendingBudgetConstructionGeneralLedgerLines.iterator(); iter.hasNext(); index++) {
453    
454                PendingBudgetConstructionGeneralLedger element = (PendingBudgetConstructionGeneralLedger) iter.next();
455                errors.addToErrorPath(linesErrorPath + "[" + index + "]");
456    
457                originalErrorCount = errors.getErrorCount();
458    
459                // run dd required field and format checks on request amount only, since only it can be changed by user
460                // no sanity checks on hiddens and readonly field params
461                validatePrimitiveFromDescriptor(element, TARGET_ERROR_PROPERTY_NAME, "", true);
462    
463                // check to see if any errors were reported
464                currentErrorCount = errors.getErrorCount();
465                isReqAmountValid = (currentErrorCount == originalErrorCount);
466                isValid &= isReqAmountValid;
467    
468                // test for new errors from this point - if none, test if benefits calc required
469                originalErrorCount = errors.getErrorCount();
470    
471                // has the request amount changed?
472                boolean isRequestAmountChanged = (isReqAmountValid && (!element.getAccountLineAnnualBalanceAmount().equals(element.getPersistedAccountLineAnnualBalanceAmount())));
473    
474                // only do checks if request amount is non-zero and not equal to currently persisted amount
475                // or the document is not budgetable and the request is non-zero
476                if (isReqAmountValid && element.getAccountLineAnnualBalanceAmount().isNonZero()) {
477    
478                    boolean isSalaryFringeLine = false;
479                    if (!isRevenue && fringeBenefitDesignatorCodesParamValues != null && element.getLaborObject() != null) {
480                        isSalaryFringeLine = fringeBenefitDesignatorCodesParamValues.contains(element.getLaborObject().getFinancialObjectFringeOrSalaryCode());
481                    }
482                    boolean is2PLG = !isRevenue && element.getFinancialObjectCode().contentEquals(KFSConstants.BudgetConstructionConstants.OBJECT_CODE_2PLG);
483                    boolean isCleanupModeActionForceCheck = budgetConstructionDocument.isCleanupModeActionForceCheck();
484    
485                    // Request notZero, do checks if user enters a change to a request amount or
486                    // (We are in cleanupMode and the current action (save or close-save) forces a cleanup mode check and
487                    // not 2PLG line and not salary fringe line)
488                    // This allows the user to use quick salary setting, monthly edit, global month delete to do cleanup work and
489                    // to print out values or push/pull before cleanup.
490                    if (isRequestAmountChanged || (!budgetConstructionDocument.isBudgetableDocument() && isCleanupModeActionForceCheck && !is2PLG && !isSalaryFringeLine)) {
491                        isValid &= this.checkPendingBudgetConstructionGeneralLedgerLine(budgetConstructionDocument, element, errors, isRevenue, false);
492                    }
493                }
494    
495                // Do RI type checks for request amount against monthly and salary setting detail if persisted amount changes
496                // or a 2plg exists and the line is a salary setting detail line
497                // Also tests if the line is has benefits associate and flags that a benefits calculation needs done.
498                // Benefits calc is then called in the form action after successful rules check and save
499                boolean forceTwoPlugRICheck = (budgetConstructionDocument.isContainsTwoPlug() && (element.getLaborObject() != null && element.getLaborObject().isDetailPositionRequiredIndicator()));
500    
501                // force monthly RI check if 2PLG and if request amount changes AND not a detail salary setting line
502                boolean forceMonthlyRICheck = (budgetConstructionDocument.isContainsTwoPlug() && (element.getLaborObject() == null || !element.getLaborObject().isDetailPositionRequiredIndicator()));
503    
504                if (isReqAmountValid && (isRequestAmountChanged || forceTwoPlugRICheck)) {
505    
506                    // check monthly for all rows
507                    if (doMonthRICheck || forceMonthlyRICheck) {
508                        if (element.getBudgetConstructionMonthly() != null && !element.getBudgetConstructionMonthly().isEmpty()) {
509    
510                            BudgetConstructionMonthly budgetConstructionMonthly = element.getBudgetConstructionMonthly().get(0);
511                            if (budgetConstructionMonthly != null) {
512                                KualiInteger monthSum = KualiInteger.ZERO;
513                                monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth1LineAmount());
514                                monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth2LineAmount());
515                                monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth3LineAmount());
516                                monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth4LineAmount());
517                                monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth5LineAmount());
518                                monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth6LineAmount());
519                                monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth7LineAmount());
520                                monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth8LineAmount());
521                                monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth9LineAmount());
522                                monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth10LineAmount());
523                                monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth11LineAmount());
524                                monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth12LineAmount());
525    
526                                if (!monthSum.equals(element.getAccountLineAnnualBalanceAmount())) {
527                                    isValid &= false;
528                                    String pkeyVal = element.getFinancialObjectCode() + "," + element.getFinancialSubObjectCode();
529                                    GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT, BCKeyConstants.ERROR_MONTHLY_SUM_REQUEST_NOT_EQUAL, pkeyVal, monthSum.toString(), element.getAccountLineAnnualBalanceAmount().toString());
530                                }
531                            }
532                        }
533                    }
534    
535                    // check salary setting detail sum if expenditure line is a ss detail line
536                    // and salary setting option is turned on
537                    if (!SpringContext.getBean(SalarySettingService.class).isSalarySettingDisabled()) {
538                        if (element.getLaborObject() != null) {
539                            if (element.getLaborObject().isDetailPositionRequiredIndicator()) {
540    
541                                // sum the detail lines and compare against the accounting line request amount
542                                KualiInteger salarySum = KualiInteger.ZERO;
543    
544                                // if salary setting detail exists, sum it otherwise default to zero
545                                if (element.isPendingBudgetConstructionAppointmentFundingExists()) {
546    
547                                    // run reportquery to get the salary request sum
548                                    salarySum = SpringContext.getBean(BudgetDocumentService.class).getPendingBudgetConstructionAppointmentFundingRequestSum(element);
549    
550                                }
551    
552                                if (!salarySum.equals(element.getAccountLineAnnualBalanceAmount())) {
553                                    isValid &= false;
554                                    String pkeyVal = element.getFinancialObjectCode() + "," + element.getFinancialSubObjectCode();
555                                    GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT, BCKeyConstants.ERROR_SALARY_SUM_REQUEST_NOT_EQUAL, pkeyVal, salarySum.toString(), element.getAccountLineAnnualBalanceAmount().toString());
556                                }
557                            }
558                        }
559                    }
560    
561                    // only do benefits calc needed test if the user changed something - not if forcing the RI check
562                    if (isReqAmountValid && !element.getAccountLineAnnualBalanceAmount().equals(element.getPersistedAccountLineAnnualBalanceAmount())) {
563    
564                        // if benefits calculation is turned on,
565                        // check if the line is benefits related and call for calculation after save
566                        if (!SpringContext.getBean(BenefitsCalculationService.class).isBenefitsCalculationDisabled()) {
567    
568                            // retest for added errors since testing this line started - if none, test if benefits calc required
569                            currentErrorCount = errors.getErrorCount();
570                            isReqAmountValid = (currentErrorCount == originalErrorCount);
571    
572                            if (isReqAmountValid && element.getPositionObjectBenefit() != null && !element.getPositionObjectBenefit().isEmpty()) {
573                                budgetConstructionDocument.setBenefitsCalcNeeded(true);
574                            }
575                        }
576                    }
577                }
578    
579                errors.removeFromErrorPath(linesErrorPath + "[" + index + "]");
580            }
581    
582            return isValid;
583        }
584    
585        /**
586         * Checks a PBGL line. Assumes the line has been checked against the dd for formatting and if required
587         * 
588         * @param budgetConstructionDocument
589         * @param pendingBudgetConstructionGeneralLedger
590         * @return
591         */
592        protected boolean checkPendingBudgetConstructionGeneralLedgerLine(BudgetConstructionDocument budgetConstructionDocument, PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger, MessageMap errors, boolean isRevenue, boolean isAdd) {
593            LOG.debug("checkPendingBudgetConstructionGeneralLedgerLine() start");
594    
595            boolean isValid = true;
596    
597            // now make sure all the necessary business objects are fully populated
598            // this refreshes any refs not done by populate for display purposes auto-update="none"
599            pendingBudgetConstructionGeneralLedger.refreshNonUpdateableReferences();
600    
601            isValid &= validatePBGLLine(pendingBudgetConstructionGeneralLedger, isAdd);
602            if (isValid) {
603    
604                // all lines must have objects defined with financialBudgetAggregation = 'O';
605                isValid &= isBudgetAggregationAllowed(budgetAggregationCodesParamValues, pendingBudgetConstructionGeneralLedger, errors, isAdd);
606    
607                isValid &= this.isBudgetAllowed(budgetConstructionDocument, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, errors, isAdd, false);
608    
609                // revenue specific checks
610                if (isRevenue) {
611    
612                    // no revenue lines in CnG accounts or SDCI
613                    isValid &= isNotSalarySettingOnly(salarySettingFundGroupsParamValues, salarySettingSubFundGroupsParamValues, budgetConstructionDocument, pendingBudgetConstructionGeneralLedger, errors, isRevenue, isAdd);
614    
615                    // line must use matching revenue object type
616                    isValid &= isObjectTypeAllowed(revenueObjectTypesParamValues, pendingBudgetConstructionGeneralLedger, errors, isRevenue, isAdd);
617    
618                }
619                else {
620                    // expenditure specific checks
621    
622                    // line must use matching expenditure object type
623                    isValid &= isObjectTypeAllowed(expenditureObjectTypesParamValues, pendingBudgetConstructionGeneralLedger, errors, isRevenue, isAdd);
624    
625                    // no lines using labor objects in non-wage accounts
626                    isValid &= isNonWagesAccountNotLaborObject(budgetConstructionDocument, pendingBudgetConstructionGeneralLedger, errors, isAdd);
627    
628                    // only lines using detail labor objects allowed in fund group CG and sfund group SDCI
629                    isValid &= isNotSalarySettingOnly(salarySettingFundGroupsParamValues, salarySettingSubFundGroupsParamValues, budgetConstructionDocument, pendingBudgetConstructionGeneralLedger, errors, isRevenue, isAdd);
630    
631                    // no lines using fringe benefit target object codes allowed to be manually added by user
632                    // the lines are created by benefits calculation process
633                    isValid &= isNotFringeBenefitObject(fringeBenefitDesignatorCodesParamValues, pendingBudgetConstructionGeneralLedger, errors, isAdd);
634                }
635    
636            }
637    
638            if (!isValid) {
639                LOG.info("business rule checks failed in checkPendingBudgetConstructionGeneralLedgerLine in BudgetConstructionRules");
640            }
641    
642            LOG.debug("checkPendingBudgetConstructionGeneralLedgerLine() end");
643            return isValid;
644        }
645    
646        protected boolean validatePBGLLine(PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger, boolean isAdd) {
647            if (pendingBudgetConstructionGeneralLedger == null) {
648                throw new IllegalStateException(getKualiConfigurationService().getPropertyString(KFSKeyConstants.ERROR_DOCUMENT_NULL_ACCOUNTING_LINE));
649            }
650    
651            // grab the service instance that will be needed by all the validate methods
652            DataDictionary dd = dataDictionaryService.getDataDictionary();
653    
654            // retrieve each pbgl line object and validate
655            boolean valid = true;
656    
657            // object code is required
658            ObjectCode objectCode = pendingBudgetConstructionGeneralLedger.getFinancialObject();
659    
660            // this code calls a local version (not AccountingLineRuleHelper) of isValidObjectCode to add the bad value to the error
661            // message
662            if (isAdd) {
663                valid &= isValidObjectCode(objectCode, pendingBudgetConstructionGeneralLedger.getFinancialObjectCode(), dd, KFSConstants.FINANCIAL_OBJECT_CODE_PROPERTY_NAME);
664            }
665            else {
666                valid &= isValidObjectCode(objectCode, pendingBudgetConstructionGeneralLedger.getFinancialObjectCode(), dd, TARGET_ERROR_PROPERTY_NAME);
667            }
668    
669            // sub object is not required
670            if (StringUtils.isNotBlank(pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode()) && !pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode().equalsIgnoreCase(KFSConstants.getDashFinancialSubObjectCode())) {
671                SubObjectCode subObjectCode = pendingBudgetConstructionGeneralLedger.getFinancialSubObject();
672    
673                // this code calls a local version (not AccountingLineRuleHelper) of isValidSubObjectCode to add the bad value to the
674                // error message
675                if (isAdd) {
676                    valid &= isValidSubObjectCode(subObjectCode, pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode(), dd, KFSConstants.FINANCIAL_SUB_OBJECT_CODE_PROPERTY_NAME);
677                }
678                else {
679                    valid &= isValidSubObjectCode(subObjectCode, pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode(), dd, TARGET_ERROR_PROPERTY_NAME);
680                }
681            }
682    
683            return valid;
684        }
685    
686        /**
687         * Validates a single primitive in a BO
688         * 
689         * @param object
690         * @param attributeName
691         * @param errorPrefix
692         * @param validateRequired
693         */
694        protected void validatePrimitiveFromDescriptor(Object object, String attributeName, String errorPrefix, boolean validateRequired) {
695    
696            try {
697                PropertyDescriptor attributeDescriptor = PropertyUtils.getPropertyDescriptor(object, attributeName);
698                validatePrimitiveFromDescriptor(object.getClass().getName(), object, attributeDescriptor, "", true);
699            }
700            catch (NoSuchMethodException e) {
701                throw new InfrastructureException("unable to find propertyDescriptor for property '" + attributeName + "'", e);
702            }
703            catch (IllegalAccessException e) {
704                throw new InfrastructureException("unable to access propertyDescriptor for property '" + attributeName + "'", e);
705            }
706            catch (InvocationTargetException e) {
707                throw new InfrastructureException("unable to invoke methods for property '" + attributeName + "'", e);
708            }
709        }
710    
711        /**
712         * Validates a primitive in a BO
713         * 
714         * @param entryName
715         * @param object
716         * @param propertyDescriptor
717         * @param errorPrefix
718         * @param validateRequired
719         */
720        protected void validatePrimitiveFromDescriptor(String entryName, Object object, PropertyDescriptor propertyDescriptor, String errorPrefix, boolean validateRequired) {
721    
722            // validate the primitive attributes if defined in the dictionary
723            if (null != propertyDescriptor && dataDictionaryService.isAttributeDefined(entryName, propertyDescriptor.getName())) {
724                Object value = ObjectUtils.getPropertyValue(object, propertyDescriptor.getName());
725                Class propertyType = propertyDescriptor.getPropertyType();
726    
727                if (TypeUtils.isStringClass(propertyType) || TypeUtils.isIntegralClass(propertyType) || TypeUtils.isDecimalClass(propertyType) || TypeUtils.isTemporalClass(propertyType)) {
728    
729                    // check value format against dictionary
730                    if (value != null && StringUtils.isNotBlank(value.toString())) {
731                        if (!TypeUtils.isTemporalClass(propertyType)) {
732                            getDictionaryValidationService().validateAttributeFormat(entryName, propertyDescriptor.getName(), value.toString(), errorPrefix + propertyDescriptor.getName());
733                        }
734                    }
735                    else if (validateRequired) {
736                        getDictionaryValidationService().validateAttributeRequired(entryName, propertyDescriptor.getName(), value, Boolean.FALSE, errorPrefix + propertyDescriptor.getName());
737                    }
738                }
739            }
740        }
741    
742        protected boolean isObjectTypeAllowed(List paramValues, PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isRevenue, boolean isAdd) {
743            boolean isAllowed = true;
744    
745            if (paramValues != null) {
746                if (!paramValues.contains(accountingLine.getFinancialObject().getFinancialObjectTypeCode())) {
747                    isAllowed = false;
748    
749                    String targetErrorProperty;
750                    if (isAdd) {
751                        targetErrorProperty = KFSPropertyConstants.FINANCIAL_OBJECT_CODE;
752                    }
753                    else {
754                        targetErrorProperty = TARGET_ERROR_PROPERTY_NAME;
755                    }
756    
757                    if (isRevenue) {
758                        this.putError(errors, targetErrorProperty, KFSKeyConstants.ERROR_DOCUMENT_EXPENSE_ON_INCOME_SIDE, isAdd, accountingLine.getFinancialObjectCode());
759                    }
760                    else {
761                        this.putError(errors, targetErrorProperty, KFSKeyConstants.ERROR_DOCUMENT_INCOME_ON_EXPENSE_SIDE, isAdd, accountingLine.getFinancialObjectCode());
762                    }
763                }
764            }
765            else {
766                isAllowed = false;
767            }
768    
769            return isAllowed;
770        }
771    
772        protected boolean isBudgetAggregationAllowed(List paramValues, PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isAdd) {
773            boolean isAllowed = true;
774    
775            if (paramValues != null) {
776                if (!paramValues.contains(accountingLine.getFinancialObject().getFinancialBudgetAggregationCd())) {
777                    isAllowed = false;
778    
779                    this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, KFSKeyConstants.ERROR_DOCUMENT_INCORRECT_OBJ_CODE_WITH_BUDGET_AGGREGATION, isAdd, accountingLine.getFinancialObjectCode(), accountingLine.getFinancialObject().getFinancialBudgetAggregationCd());
780                }
781            }
782            else {
783                isAllowed = false;
784            }
785    
786            return isAllowed;
787        }
788    
789        protected boolean isNewLineUnique(BudgetConstructionDocument budgetConstructionDocument, PendingBudgetConstructionGeneralLedger newLine, MessageMap errors, boolean isRevenue) {
790            boolean isUnique = true;
791            List<PendingBudgetConstructionGeneralLedger> existingLines;
792    
793            if (isRevenue) {
794                existingLines = budgetConstructionDocument.getPendingBudgetConstructionGeneralLedgerRevenueLines();
795            }
796            else {
797                existingLines = budgetConstructionDocument.getPendingBudgetConstructionGeneralLedgerExpenditureLines();
798            }
799    
800            if (BudgetConstructionRuleUtil.hasExistingPBGLLine(existingLines, newLine)) {
801                isUnique = false;
802                errors.putError(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_BUDGET_LINE_EXISTS, newLine.getFinancialObjectCode() + "," + newLine.getFinancialSubObjectCode());
803            }
804    
805            return isUnique;
806        }
807    
808        protected boolean isNonWagesAccountNotLaborObject(BudgetConstructionDocument budgetConstructionDocument, PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isAdd) {
809            boolean isAllowed = true;
810    
811            if (budgetConstructionDocument.getAccount().getSubFundGroup() == null || !budgetConstructionDocument.getAccount().getSubFundGroup().isSubFundGroupWagesIndicator()) {
812                if (accountingLine.getLaborObject() != null) {
813                    isAllowed = false;
814                    this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_LABOR_OBJECT_IN_NOWAGES_ACCOUNT, isAdd, accountingLine.getFinancialObjectCode());
815                }
816            }
817            return isAllowed;
818        }
819    
820        protected boolean isNotFringeBenefitObject(List paramValues, PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isAdd) {
821            boolean isAllowed = true;
822    
823            if (paramValues != null) {
824                if (accountingLine.getLaborObject() != null) {
825                    if (paramValues.contains(accountingLine.getLaborObject().getFinancialObjectFringeOrSalaryCode())) {
826                        isAllowed = false;
827                        this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_FRINGE_BENEFIT_OBJECT_NOT_ALLOWED, isAdd, accountingLine.getFinancialObjectCode());
828                    }
829                }
830            }
831            else {
832                isAllowed = false;
833            }
834    
835            return isAllowed;
836        }
837    
838        protected boolean isNotSalarySettingOnly(List fundGroupParamValues, List subfundGroupParamValues, BudgetConstructionDocument budgetConstructionDocument, PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isRevenue, boolean isAdd) {
839            boolean isAllowed = true;
840    
841            // check if account belongs to a fund or subfund that only allows salary setting lines
842            AccountSalarySettingOnlyCause retVal = budgetParameterService.isSalarySettingOnlyAccount(budgetConstructionDocument);
843            if (retVal != AccountSalarySettingOnlyCause.MISSING_PARAM) {
844                if (retVal != AccountSalarySettingOnlyCause.NONE) {
845    
846                    // the line must use an object that is a detail salary labor object
847                    if (isRevenue || accountingLine.getLaborObject() == null || !accountingLine.getLaborObject().isDetailPositionRequiredIndicator()) {
848    
849                        isAllowed = false;
850                        if (retVal == AccountSalarySettingOnlyCause.FUND || retVal == AccountSalarySettingOnlyCause.FUND_AND_SUBFUND) {
851                            this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_SALARY_SETTING_OBJECT_ONLY, isAdd, "fund " + budgetConstructionDocument.getAccount().getSubFundGroup().getFundGroupCode());
852    
853                        }
854                        if (retVal == AccountSalarySettingOnlyCause.SUBFUND || retVal == AccountSalarySettingOnlyCause.FUND_AND_SUBFUND) {
855                            this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_SALARY_SETTING_OBJECT_ONLY, isAdd, "subfund " + budgetConstructionDocument.getAccount().getSubFundGroup().getSubFundGroupCode());
856                        }
857                    }
858                }
859    
860            }
861            else {
862                // missing system parameter
863                this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_SALARY_SETTING_OBJECT_ONLY_NO_PARAMETER, isAdd, budgetConstructionDocument.getAccount().getSubFundGroup().getFundGroupCode() + "," + budgetConstructionDocument.getAccount().getSubFundGroup().getSubFundGroupCode());
864                isAllowed = false;
865            }
866    
867            return isAllowed;
868        }
869    
870        /**
871         * runs rule checks that don't allow a budget
872         * 
873         * @param budgetConstructionDocument
874         * @param propertyName
875         * @param errors
876         * @param isAdd
877         * @param isDocumentAdd
878         * @return
879         */
880        protected boolean isBudgetAllowed(BudgetConstructionDocument budgetConstructionDocument, String propertyName, MessageMap errors, boolean isAdd, boolean isDocumentAdd) {
881            boolean isAllowed = true;
882            SimpleDateFormat tdf = new SimpleDateFormat("MM/dd/yyyy hh:mm a");
883    
884            // is account closed?
885            if (!budgetConstructionDocument.getAccount().isActive()) {
886                isAllowed = false;
887                this.putError(errors, propertyName, KFSKeyConstants.ERROR_CLOSED, isAdd, "account: " + budgetConstructionDocument.getAccountNumber());
888            }
889    
890            // is account expiration no budget allowed, currently < 1/1/(byfy-2)?
891            Calendar expDate = BudgetConstructionRuleUtil.getNoBudgetAllowedExpireDate(budgetConstructionDocument.getUniversityFiscalYear());
892            if (budgetConstructionDocument.getAccount().isExpired(expDate)) {
893                isAllowed = false;
894                this.putError(errors, propertyName, BCKeyConstants.ERROR_NO_BUDGET_ALLOWED, isAdd, budgetConstructionDocument.getAccountNumber(), tdf.format(budgetConstructionDocument.getAccount().getAccountExpirationDate()));
895    
896            }
897    
898            // is account a cash control account
899            if (budgetConstructionDocument.getAccount().getBudgetRecordingLevelCode().equalsIgnoreCase(BCConstants.BUDGET_RECORDING_LEVEL_N)) {
900                isAllowed = false;
901                this.putError(errors, propertyName, BCKeyConstants.ERROR_BUDGET_RECORDING_LEVEL_NOT_ALLOWED, isAdd, budgetConstructionDocument.getAccountNumber(), BCConstants.BUDGET_RECORDING_LEVEL_N);
902            }
903    
904            // grab the service instance that will be needed by all the validate methods
905            DataDictionary dd = dataDictionaryService.getDataDictionary();
906    
907            if (StringUtils.isNotBlank(budgetConstructionDocument.getSubAccountNumber()) && !budgetConstructionDocument.getSubAccountNumber().equalsIgnoreCase(KFSConstants.getDashSubAccountNumber())) {
908                SubAccount subAccount = budgetConstructionDocument.getSubAccount();
909    
910                // is subacct inactive or not exist?
911                // this code calls a local version (not AccountingLineRuleHelper) of isValidSubAccount
912                // to add the bad value to the error message
913                if (isAdd) {
914                    if (isDocumentAdd) {
915                        isAllowed &= this.isValidSubAccount(subAccount, budgetConstructionDocument.getSubAccountNumber(), dd, KFSPropertyConstants.SUB_ACCOUNT_NUMBER);
916                    }
917                    else {
918                        isAllowed &= this.isValidSubAccount(subAccount, budgetConstructionDocument.getSubAccountNumber(), dd, propertyName);
919                    }
920                }
921                else {
922                    isAllowed &= this.isValidSubAccount(subAccount, budgetConstructionDocument.getSubAccountNumber(), dd, TARGET_ERROR_PROPERTY_NAME);
923                }
924    
925                // is subacct type cost share?
926                // this hack is here since kuldev is missing one to one instances
927                // and the RI ojb mapping produces an error when attempting to test if the
928                // A21SubAccount attached to the document's SubAccount is null
929                Map<String, Object> searchCriteria = new HashMap<String, Object>();
930                searchCriteria.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, budgetConstructionDocument.getChartOfAccountsCode());
931                searchCriteria.put(KFSPropertyConstants.ACCOUNT_NUMBER, budgetConstructionDocument.getAccountNumber());
932                searchCriteria.put(KFSPropertyConstants.SUB_ACCOUNT_NUMBER, budgetConstructionDocument.getSubAccountNumber());
933                A21SubAccount a21SubAccount = (A21SubAccount) businessObjectService.findByPrimaryKey(A21SubAccount.class, searchCriteria);
934                if (ObjectUtils.isNotNull(a21SubAccount)) {
935                    if (a21SubAccount.getSubAccountTypeCode().equalsIgnoreCase(KFSConstants.SubAccountType.COST_SHARE)) {
936                        isAllowed = false;
937                        this.putError(errors, propertyName, BCKeyConstants.ERROR_SUB_ACCOUNT_TYPE_NOT_ALLOWED, isAdd, budgetConstructionDocument.getAccountNumber(), KFSConstants.SubAccountType.COST_SHARE);
938                    }
939                }
940            }
941    
942            return isAllowed;
943        }
944    
945        public boolean isValidAccount(Account account, String value, DataDictionary dataDictionary, String errorPropertyName) {
946            String label = dataDictionary.getBusinessObjectEntry(Account.class.getName()).getAttributeDefinition(KFSConstants.ACCOUNT_NUMBER_PROPERTY_NAME).getShortLabel();
947    
948            // make sure it exists
949            if (ObjectUtils.isNull(account)) {
950                GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_EXISTENCE, label + ":" + value);
951                return false;
952            }
953    
954            return true;
955        }
956    
957        public boolean isValidSubAccount(SubAccount subAccount, String value, DataDictionary dataDictionary, String errorPropertyName) {
958            String label = dataDictionary.getBusinessObjectEntry(SubAccount.class.getName()).getAttributeDefinition(KFSConstants.SUB_ACCOUNT_NUMBER_PROPERTY_NAME).getShortLabel();
959    
960            // make sure it exists
961            if (ObjectUtils.isNull(subAccount)) {
962                GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_EXISTENCE, label + ":" + value);
963                return false;
964            }
965    
966            // check to make sure it is active
967            if (!subAccount.isActive()) {
968                GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_DOCUMENT_SUB_ACCOUNT_INACTIVE, label + ":" + value);
969                return false;
970            }
971    
972            return true;
973        }
974    
975        /**
976         * Runs existence and active tests on the SubObjectCode reference This method is differenct than the one in
977         * AccountingLineRuleHelper in that it adds the bad value to the errormessage This method signature should probably be added to
978         * AccountingLineRuleHelper
979         * 
980         * @param subObjectCode
981         * @param value
982         * @param dataDictionary
983         * @param errorPropertyName
984         * @return
985         */
986        public boolean isValidSubObjectCode(SubObjectCode subObjectCode, String value, DataDictionary dataDictionary, String errorPropertyName) {
987            String label = dataDictionary.getBusinessObjectEntry(SubObjectCode.class.getName()).getAttributeDefinition(KFSConstants.FINANCIAL_SUB_OBJECT_CODE_PROPERTY_NAME).getShortLabel();
988    
989            // make sure it exists
990            if (ObjectUtils.isNull(subObjectCode)) {
991                GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_EXISTENCE, label + ":" + value);
992                return false;
993            }
994    
995            // check active flag
996            if (!subObjectCode.isActive()) {
997                GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_INACTIVE, label + ":" + value);
998                return false;
999            }
1000            return true;
1001        }
1002    
1003        /**
1004         * Runs existence and active tests on the ObjectCode reference This method is differenct than the one in
1005         * AccountingLineRuleHelper in that it adds the bad value to the errormessage This method signature should probably be added to
1006         * AccountingLineRuleHelper
1007         * 
1008         * @param objectCode
1009         * @param value
1010         * @param dataDictionary
1011         * @param errorPropertyName
1012         * @return
1013         */
1014        public boolean isValidObjectCode(ObjectCode objectCode, String value, DataDictionary dataDictionary, String errorPropertyName) {
1015            String label = dataDictionary.getBusinessObjectEntry(ObjectCode.class.getName()).getAttributeDefinition(KFSConstants.FINANCIAL_OBJECT_CODE_PROPERTY_NAME).getShortLabel();
1016    
1017            // make sure it exists
1018            if (ObjectUtils.isNull(objectCode)) {
1019                GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_EXISTENCE, label + ":" + value);
1020                return false;
1021            }
1022    
1023            // check active status
1024            if (!objectCode.isFinancialObjectActiveCode()) {
1025                GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_INACTIVE, label + ":" + value);
1026                return false;
1027            }
1028    
1029            return true;
1030        }
1031    
1032        /**
1033         * puts error to errormap for propertyName if isAdd, otherwise the property name is replaced with value of
1034         * TARGET_ERROR_PROPERTY_NAME
1035         * 
1036         * @param propertyName
1037         * @param errorKey
1038         * @param isAdd
1039         * @param errorParameters
1040         */
1041        protected void putError(MessageMap errors, String propertyName, String errorKey, boolean isAdd, String... errorParameters) {
1042    
1043            if (isAdd) {
1044                errors.putError(propertyName, errorKey, errorParameters);
1045            }
1046            else {
1047                errors.putError(TARGET_ERROR_PROPERTY_NAME, errorKey, errorParameters);
1048            }
1049    
1050        }
1051    }