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.coa.document.validation.impl;
017    
018    import java.sql.Date;
019    import java.sql.Timestamp;
020    import java.util.Calendar;
021    import java.util.Collection;
022    import java.util.HashMap;
023    import java.util.List;
024    import java.util.Map;
025    
026    import org.apache.commons.lang.StringUtils;
027    import org.apache.commons.lang.time.DateUtils;
028    import org.kuali.kfs.coa.businessobject.Account;
029    import org.kuali.kfs.coa.businessobject.AccountDescription;
030    import org.kuali.kfs.coa.businessobject.AccountGuideline;
031    import org.kuali.kfs.coa.businessobject.FundGroup;
032    import org.kuali.kfs.coa.businessobject.IndirectCostRecoveryRateDetail;
033    import org.kuali.kfs.coa.businessobject.SubFundGroup;
034    import org.kuali.kfs.coa.service.AccountService;
035    import org.kuali.kfs.coa.service.SubFundGroupService;
036    import org.kuali.kfs.gl.service.BalanceService;
037    import org.kuali.kfs.integration.cg.ContractsAndGrantsModuleService;
038    import org.kuali.kfs.integration.ld.LaborModuleService;
039    import org.kuali.kfs.sys.KFSConstants;
040    import org.kuali.kfs.sys.KFSKeyConstants;
041    import org.kuali.kfs.sys.KFSPropertyConstants;
042    import org.kuali.kfs.sys.businessobject.Building;
043    import org.kuali.kfs.sys.context.SpringContext;
044    import org.kuali.kfs.sys.document.validation.impl.KfsMaintenanceDocumentRuleBase;
045    import org.kuali.kfs.sys.service.GeneralLedgerPendingEntryService;
046    import org.kuali.kfs.sys.service.UniversityDateService;
047    import org.kuali.rice.kim.bo.Person;
048    import org.kuali.rice.kns.document.MaintenanceDocument;
049    import org.kuali.rice.kns.service.DataDictionaryService;
050    import org.kuali.rice.kns.service.DictionaryValidationService;
051    import org.kuali.rice.kns.service.ParameterEvaluator;
052    import org.kuali.rice.kns.service.ParameterService;
053    import org.kuali.rice.kns.util.GlobalVariables;
054    import org.kuali.rice.kns.util.MessageMap;
055    import org.kuali.rice.kns.util.ObjectUtils;
056    
057    /**
058     * Business rule(s) applicable to AccountMaintenance documents.
059     */
060    public class AccountRule extends KfsMaintenanceDocumentRuleBase {
061    
062        protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AccountRule.class);
063    
064        protected static final String ACCT_PREFIX_RESTRICTION = "PREFIXES";
065        protected static final String ACCT_CAPITAL_SUBFUNDGROUP = "CAPITAL_SUB_FUND_GROUPS";
066    
067        protected static final String GENERAL_FUND_CD = "GF";
068        protected static final String RESTRICTED_FUND_CD = "RF";
069        protected static final String ENDOWMENT_FUND_CD = "EN";
070        protected static final String PLANT_FUND_CD = "PF";
071    
072        protected static final String RESTRICTED_CD_RESTRICTED = "R";
073        protected static final String RESTRICTED_CD_UNRESTRICTED = "U";
074        protected static final String RESTRICTED_CD_TEMPORARILY_RESTRICTED = "T";
075        protected static final String BUDGET_RECORDING_LEVEL_MIXED = "M";
076    
077        protected static SubFundGroupService subFundGroupService;
078        protected static ParameterService parameterService;    
079        
080        protected GeneralLedgerPendingEntryService generalLedgerPendingEntryService;
081        protected BalanceService balanceService;
082        protected AccountService accountService;
083        protected ContractsAndGrantsModuleService contractsAndGrantsModuleService;
084    
085        protected Account oldAccount;
086        protected Account newAccount;
087    
088        public AccountRule() {
089    
090            // Pseudo-inject some services.
091            //
092            // This approach is being used to make it simpler to convert the Rule classes
093            // to spring-managed with these services injected by Spring at some later date.
094            // When this happens, just remove these calls to the setters with
095            // SpringContext, and configure the bean defs for spring.
096            this.setGeneralLedgerPendingEntryService(SpringContext.getBean(GeneralLedgerPendingEntryService.class));
097            this.setBalanceService(SpringContext.getBean(BalanceService.class));
098            this.setAccountService(SpringContext.getBean(AccountService.class));
099            this.setContractsAndGrantsModuleService(SpringContext.getBean(ContractsAndGrantsModuleService.class));
100        }
101    
102        /**
103         * This method sets the convenience objects like newAccount and oldAccount, so you have short and easy handles to the new and
104         * old objects contained in the maintenance document. It also calls the BusinessObjectBase.refresh(), which will attempt to load
105         * all sub-objects from the DB by their primary keys, if available.
106         */
107        public void setupConvenienceObjects() {
108    
109            // setup oldAccount convenience objects, make sure all possible sub-objects are populated
110            oldAccount = (Account) super.getOldBo();
111    
112            // setup newAccount convenience objects, make sure all possible sub-objects are populated
113            newAccount = (Account) super.getNewBo();
114        }
115    
116        /**
117         * This method calls the route rules but does not fail if any of them fail (this only happens on routing)
118         * 
119         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
120         */
121        protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) {
122    
123            LOG.info("processCustomSaveDocumentBusinessRules called");
124            // call the route rules to report all of the messages, but ignore the result
125            processCustomRouteDocumentBusinessRules(document);
126    
127            // Save always succeeds, even if there are business rule failures
128            return true;
129        }
130    
131        /**
132         * This method calls the following rules: checkAccountGuidelinesValidation checkEmptyValues checkGeneralRules checkCloseAccount
133         * checkContractsAndGrants checkExpirationDate checkFundGroup checkSubFundGroup checkFiscalOfficerIsValidKualiUser this rule
134         * will fail on routing
135         * 
136         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomRouteDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
137         */
138        protected boolean processCustomRouteDocumentBusinessRules(MaintenanceDocument document) {
139    
140            LOG.info("processCustomRouteDocumentBusinessRules called");
141            setupConvenienceObjects();
142    
143            // default to success
144            boolean success = true;
145    
146            // validate the embedded AccountGuideline object
147            success &= checkAccountGuidelinesValidation(newAccount.getAccountGuideline());
148    
149            success &= checkEmptyValues(document);
150            success &= checkGeneralRules(document);
151            success &= checkCloseAccount(document);
152            success &= checkContractsAndGrants(document);
153            success &= checkExpirationDate(document);
154            success &= checkFundGroup(document);
155            success &= checkSubFundGroup(document);
156            success &= checkIncomeStreamAccountRule();
157            success &= checkUniqueAccountNumber(document);
158            
159            return success;
160        }
161    
162        /**
163         * This method checks the basic rules for empty values in an account and associated objects with this account If guidelines are
164         * required for this Business Object it checks to make sure that it is filled out It also checks for partially filled out
165         * reference keys on the following: continuationAccount incomeStreamAccount endowmentIncomeAccount reportsToAccount
166         * contractControlAccount indirectCostRecoveryAcct
167         * 
168         * @param maintenanceDocument
169         * @return false if any of these are empty
170         */
171        protected boolean checkEmptyValues(MaintenanceDocument maintenanceDocument) {
172    
173            LOG.info("checkEmptyValues called");
174    
175            boolean success = true;
176    
177            // guidelines are always required, except when the expirationDate is set, and its
178            // earlier than today
179            boolean guidelinesRequired = areGuidelinesRequired((Account) maintenanceDocument.getNewMaintainableObject().getBusinessObject());
180    
181            // confirm that required guidelines are entered, if required
182            if (guidelinesRequired) {
183                success &= checkEmptyBOField("accountGuideline.accountExpenseGuidelineText", newAccount.getAccountGuideline().getAccountExpenseGuidelineText(), "Expense Guideline");
184                success &= checkEmptyBOField("accountGuideline.accountIncomeGuidelineText", newAccount.getAccountGuideline().getAccountIncomeGuidelineText(), "Income Guideline");
185                success &= checkEmptyBOField("accountGuideline.accountPurposeText", newAccount.getAccountGuideline().getAccountPurposeText(), "Account Purpose");
186            }
187    
188            // this set confirms that all fields which are grouped (ie, foreign keys of a reference
189            // object), must either be none filled out, or all filled out.
190            success &= checkForPartiallyFilledOutReferenceForeignKeys("continuationAccount");
191            success &= checkForPartiallyFilledOutReferenceForeignKeys("incomeStreamAccount");
192            success &= checkForPartiallyFilledOutReferenceForeignKeys("endowmentIncomeAccount");
193            success &= checkForPartiallyFilledOutReferenceForeignKeys("reportsToAccount");
194            success &= checkForPartiallyFilledOutReferenceForeignKeys("contractControlAccount");
195            success &= checkForPartiallyFilledOutReferenceForeignKeys("indirectCostRecoveryAcct");
196    
197            return success;
198        }
199    
200        /**
201         * This method validates that the account guidelines object is valid
202         * 
203         * @param accountGuideline
204         * @return true if account guideline is valid
205         */
206        protected boolean checkAccountGuidelinesValidation(AccountGuideline accountGuideline) {
207            MessageMap map = GlobalVariables.getMessageMap();
208            int errorCount = map.getErrorCount();
209            GlobalVariables.getMessageMap().addToErrorPath("document.newMaintainableObject.accountGuideline");
210            dictionaryValidationService.validateBusinessObject(accountGuideline, false);
211            GlobalVariables.getMessageMap().removeFromErrorPath("document.newMaintainableObject.accountGuideline");
212            return map.getErrorCount() == errorCount;
213        }
214    
215        /**
216         * This method determines whether the guidelines are required, based on business rules.
217         * 
218         * @param account - the populated Account bo to be evaluated
219         * @return true if guidelines are required, false otherwise
220         */
221        protected boolean areGuidelinesRequired(Account account) {
222    
223            boolean result = true;
224    
225            if (account.getAccountExpirationDate() != null) {
226                Timestamp today = getDateTimeService().getCurrentTimestamp();
227                today.setTime(DateUtils.truncate(today, Calendar.DAY_OF_MONTH).getTime());
228                if (account.getAccountExpirationDate().before(today)) {
229                    result = false;
230                }
231            }
232            return result;
233        }
234    
235        /**
236         * This method tests whether the accountNumber passed in is prefixed with an allowed prefix, or an illegal one. The illegal
237         * prefixes are passed in as an array of strings.
238         * 
239         * @param accountNumber - The Account Number to be tested.
240         * @param illegalValues - An Array of Strings of the unallowable prefixes.
241         * @return false if the accountNumber starts with any of the illegalPrefixes, true otherwise
242         */
243        protected boolean accountNumberStartsWithAllowedPrefix(String accountNumber, List<String> illegalValues) {
244            boolean result = true;
245            for (String illegalValue : illegalValues) {
246                if (accountNumber.startsWith(illegalValue)) {
247                    result = false;
248                    putFieldError("accountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_NMBR_NOT_ALLOWED, new String[] { accountNumber, illegalValue });
249                }
250            }
251            return result;
252        }
253    
254        /**
255         * This method tests whether an account is being ReOpened by anyone except a system supervisor. Only system supervisors may
256         * reopen closed accounts.
257         * 
258         * @param document - populated document containing the old and new accounts
259         * @param user - the user who is trying to possibly reopen the account
260         * @return true if: document is an edit document, old was closed and new is open, and the user is not one of the System
261         *         Supervisors
262         */
263        protected boolean isNonSystemSupervisorEditingAClosedAccount(MaintenanceDocument document, Person user) {
264            if (document.isEdit()) {
265                // do the test
266                if (oldAccount.isClosed() ) {
267                    return !getDocumentHelperService().getDocumentAuthorizer(document).isAuthorized(document, KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.EDIT_INACTIVE_ACCOUNT, user.getPrincipalId());
268                }
269                return false;
270            }
271            return false;
272        }
273    
274        /**
275         * This method tests whether a given account has the T - Temporary value for Restricted Status Code, but does not have a
276         * Restricted Status Date, which is required when the code is T.
277         * 
278         * @param account
279         * @return true if the account is temporarily restricted but the status date is empty
280         */
281        protected boolean hasTemporaryRestrictedStatusCodeButNoRestrictedStatusDate(Account account) {
282    
283            boolean result = false;
284    
285            if (StringUtils.isNotBlank(account.getAccountRestrictedStatusCode())) {
286                if (RESTRICTED_CD_TEMPORARILY_RESTRICTED.equalsIgnoreCase(account.getAccountRestrictedStatusCode().trim())) {
287                    if (account.getAccountRestrictedStatusDate() == null) {
288                        result = true;
289                    }
290                }
291            }
292            return result;
293        }
294    
295        /**
296         * Checks whether the account restricted status code is the default from the sub fund group.
297         * 
298         * @param account
299         * @return true if the restricted status code is the same as the sub fund group's
300         */
301        protected boolean hasDefaultRestrictedStatusCode(Account account) {
302            boolean result = false;
303    
304            if (StringUtils.isNotBlank(account.getAccountRestrictedStatusCode())) {
305                result = account.getAccountRestrictedStatusCode().equals(account.getSubFundGroup().getAccountRestrictedStatusCode());
306            }
307    
308            return result;
309        }
310    
311        /**
312         * This method checks some of the general business rules associated with this document Calls the following rules:
313         * accountNumberStartsWithAllowedPrefix isNonSystemSupervisorEditingAClosedAccount
314         * hasTemporaryRestrictedStatusCodeButNoRestrictedStatusDate checkFringeBenefitAccountRule checkUserStatusAndType (on fiscal
315         * officer, supervisor and manager) ensures that the fiscal officer, supervisor and manager are not the same
316         * isContinuationAccountExpired
317         * 
318         * @param maintenanceDocument
319         * @return false on rules violation
320         */
321        protected boolean checkGeneralRules(MaintenanceDocument maintenanceDocument) {
322    
323            LOG.info("checkGeneralRules called");
324            Person fiscalOfficer = newAccount.getAccountFiscalOfficerUser();
325            Person accountManager = newAccount.getAccountManagerUser();
326            Person accountSupervisor = newAccount.getAccountSupervisoryUser();
327    
328            boolean success = true;
329    
330            // Enforce institutionally specified restrictions on account number prefixes
331            // (e.g. the account number cannot begin with a 3 or with 00.)
332            // Only bother trying if there is an account string to test
333            if (!StringUtils.isBlank(newAccount.getAccountNumber())) {
334                // test the number
335                success &= accountNumberStartsWithAllowedPrefix(newAccount.getAccountNumber(), getParameterService().getParameterValues(Account.class, ACCT_PREFIX_RESTRICTION));
336            }
337    
338            // only a FIS supervisor can reopen a closed account. (This is the central super user, not an account supervisor).
339            // we need to get the old maintanable doc here
340            if (isNonSystemSupervisorEditingAClosedAccount(maintenanceDocument, GlobalVariables.getUserSession().getPerson())) {
341                success &= false;
342                putFieldError("closed", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ONLY_SUPERVISORS_CAN_EDIT);
343            }
344            
345            // check FringeBenefit account rules
346            success &= checkFringeBenefitAccountRule(newAccount);
347    
348            if (ObjectUtils.isNotNull(fiscalOfficer) && !getDocumentHelperService().getDocumentAuthorizer(maintenanceDocument).isAuthorized(maintenanceDocument, KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_FISCAL_OFFICER, fiscalOfficer.getPrincipalId())) {
349                super.putFieldError("accountFiscalOfficerUser.principalName", KFSKeyConstants.ERROR_USER_MISSING_PERMISSION, new String[] {fiscalOfficer.getName(), KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_FISCAL_OFFICER});
350                            success = false;
351            }
352            if (ObjectUtils.isNotNull(accountSupervisor) && !getDocumentHelperService().getDocumentAuthorizer(maintenanceDocument).isAuthorized(maintenanceDocument, KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_ACCOUNT_SUPERVISOR, accountSupervisor.getPrincipalId())) {
353                super.putFieldError("accountSupervisoryUser.principalName", KFSKeyConstants.ERROR_USER_MISSING_PERMISSION, new String[] {accountSupervisor.getName(), KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_ACCOUNT_SUPERVISOR});
354                            success = false;
355            }
356            if (ObjectUtils.isNotNull(accountManager) && !getDocumentHelperService().getDocumentAuthorizer(maintenanceDocument).isAuthorized(maintenanceDocument, KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_ACCOUNT_MANAGER, accountManager.getPrincipalId())) {
357                super.putFieldError("accountManagerUser.principalName", KFSKeyConstants.ERROR_USER_MISSING_PERMISSION, new String[] {accountManager.getName(), KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_ACCOUNT_MANAGER});
358                            success = false;
359            }
360    
361            // the supervisor cannot be the same as the fiscal officer or account manager.
362            if (isSupervisorSameAsFiscalOfficer(newAccount)) {
363                success &= false;
364                putFieldError("accountsSupervisorySystemsIdentifier", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_SUPER_CANNOT_BE_FISCAL_OFFICER);
365            }
366            if (isSupervisorSameAsManager(newAccount)) {
367                success &= false;
368                putFieldError("accountManagerSystemIdentifier", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_SUPER_CANNOT_BE_ACCT_MGR);
369            }
370    
371            // disallow continuation account being expired
372            if (isContinuationAccountExpired(newAccount)) {
373                success &= false;
374                putFieldError("continuationAccountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCOUNT_EXPIRED_CONTINUATION);
375            }
376    
377            return success;
378        }
379    
380        /**
381         * This method tests whether the continuation account entered (if any) has expired or not.
382         * 
383         * @param newAccount
384         * @return true if continuation account has expired
385         */
386        protected boolean isContinuationAccountExpired(Account newAccount) {
387    
388            boolean result = false;
389    
390            String chartCode = newAccount.getContinuationFinChrtOfAcctCd();
391            String accountNumber = newAccount.getContinuationAccountNumber();
392    
393            // if either chartCode or accountNumber is not entered, then we
394            // can't continue, so exit
395            if (StringUtils.isBlank(chartCode) || StringUtils.isBlank(accountNumber)) {
396                return result;
397            }
398    
399            // attempt to retrieve the continuation account from the DB
400            Account continuation = accountService.getByPrimaryId(chartCode, accountNumber);
401    
402            // if the object doesn't exist, then we can't continue, so exit
403            if (ObjectUtils.isNull(continuation)) {
404                return result;
405            }
406    
407            // at this point, we have a valid continuation account, so we just need to
408            // know whether its expired or not
409            result = continuation.isExpired();
410    
411            return result;
412        }
413    
414        /**
415         * the fringe benefit account (otherwise known as the reportsToAccount) is required if the fringe benefit code is set to N. The
416         * fringe benefit code of the account designated to accept the fringes must be Y.
417         * 
418         * @param newAccount
419         * @return
420         */
421        protected boolean checkFringeBenefitAccountRule(Account newAccount) {
422    
423            boolean result = true;
424    
425            // if this account is selected as a Fringe Benefit Account, then we have nothing
426            // to test, so exit
427            if (newAccount.isAccountsFringesBnftIndicator()) {
428                return true;
429            }
430    
431            // if fringe benefit is not selected ... continue processing
432    
433            // fringe benefit account number is required
434            if (StringUtils.isBlank(newAccount.getReportsToAccountNumber())) {
435                putFieldError("reportsToAccountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_RPTS_TO_ACCT_REQUIRED_IF_FRINGEBENEFIT_FALSE);
436                result &= false;
437            }
438    
439            // fringe benefit chart of accounts code is required
440            if (StringUtils.isBlank(newAccount.getReportsToChartOfAccountsCode())) {
441                putFieldError("reportsToChartOfAccountsCode", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_RPTS_TO_ACCT_REQUIRED_IF_FRINGEBENEFIT_FALSE);
442                result &= false;
443            }
444    
445            // if either of the fringe benefit account fields are not present, then we're done
446            if (result == false) {
447                return result;
448            }
449    
450            // attempt to load the fringe benefit account
451            Account fringeBenefitAccount = accountService.getByPrimaryId(newAccount.getReportsToChartOfAccountsCode(), newAccount.getReportsToAccountNumber());
452    
453            // fringe benefit account must exist
454            if (fringeBenefitAccount == null) {
455                putFieldError("reportsToAccountNumber", KFSKeyConstants.ERROR_EXISTENCE, getFieldLabel(Account.class, "reportsToAccountNumber"));
456                return false;
457            }
458    
459            // fringe benefit account must be active
460            if (!fringeBenefitAccount.isActive()) {
461                putFieldError("reportsToAccountNumber", KFSKeyConstants.ERROR_INACTIVE, getFieldLabel(Account.class, "reportsToAccountNumber"));
462                result &= false;
463            }
464    
465            // make sure the fringe benefit account specified is set to fringe benefits = Y
466            if (!fringeBenefitAccount.isAccountsFringesBnftIndicator()) {
467                putFieldError("reportsToAccountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_RPTS_TO_ACCT_MUST_BE_FLAGGED_FRINGEBENEFIT, fringeBenefitAccount.getChartOfAccountsCode() + "-" + fringeBenefitAccount.getAccountNumber());
468                result &= false;
469            }
470    
471            return result;
472        }
473    
474        /**
475         * This method is a helper method for checking if the supervisor user is the same as the fiscal officer Calls
476         * {@link AccountRule#areTwoUsersTheSame(Person, Person)}
477         * 
478         * @param accountGlobals
479         * @return true if the two users are the same
480         */
481        protected boolean isSupervisorSameAsFiscalOfficer(Account account) {
482            return areTwoUsersTheSame(account.getAccountSupervisoryUser(), account.getAccountFiscalOfficerUser());
483        }
484    
485        /**
486         * This method is a helper method for checking if the supervisor user is the same as the manager Calls
487         * {@link AccountRule#areTwoUsersTheSame(Person, Person)}
488         * 
489         * @param accountGlobals
490         * @return true if the two users are the same
491         */
492        protected boolean isSupervisorSameAsManager(Account account) {
493            return areTwoUsersTheSame(account.getAccountSupervisoryUser(), account.getAccountManagerUser());
494        }
495    
496        /**
497         * This method checks to see if two users are the same Person using their identifiers
498         * 
499         * @param user1
500         * @param user2
501         * @return true if these two users are the same
502         */
503        protected boolean areTwoUsersTheSame(Person user1, Person user2) {
504            if (ObjectUtils.isNull(user1) || user1.getPrincipalId() == null ) {
505                return false;
506            }
507            if (ObjectUtils.isNull(user2) || user2.getPrincipalId() == null ) {
508                return false;
509            }
510            return user1.getPrincipalId().equals(user2.getPrincipalId());
511        }
512    
513        /**
514         * This method checks to see if the user is trying to close the account and if so if any rules are being violated Calls the
515         * additional rule checkAccountExpirationDateValidTodayOrEarlier
516         * 
517         * @param maintenanceDocument
518         * @return false on rules violation
519         */
520        protected boolean checkCloseAccount(MaintenanceDocument maintenanceDocument) {
521    
522            LOG.info("checkCloseAccount called");
523    
524            boolean success = true;
525            boolean isBeingClosed = false;
526    
527            // if the account isnt being closed, then dont bother processing the rest of
528            // the method
529            if (oldAccount.isActive() && !newAccount.isActive()) {
530                isBeingClosed = true;
531            }
532    
533            if (!isBeingClosed) {
534                return true;
535            }
536    
537            // on an account being closed, the expiration date must be
538            success &= checkAccountExpirationDateValidTodayOrEarlier(newAccount);
539    
540            // when closing an account, a continuation account is required
541            if (StringUtils.isBlank(newAccount.getContinuationAccountNumber())) {
542                putFieldError("continuationAccountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_CLOSE_CONTINUATION_ACCT_REQD);
543                success &= false;
544            }
545            if (StringUtils.isBlank(newAccount.getContinuationFinChrtOfAcctCd())) {
546                putFieldError("continuationFinChrtOfAcctCd", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_CLOSE_CONTINUATION_ACCT_REQD);
547                success &= false;
548            }
549    
550            // must have no pending ledger entries
551            if (generalLedgerPendingEntryService.hasPendingGeneralLedgerEntry(newAccount)) {
552                putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCOUNT_CLOSED_PENDING_LEDGER_ENTRIES);
553                success &= false;
554            }
555    
556            // beginning balance must be loaded in order to close account
557            if (!balanceService.beginningBalanceLoaded(newAccount)) {
558                putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCOUNT_CLOSED_NO_LOADED_BEGINNING_BALANCE);
559                success &= false;
560            }
561    
562            // must have no base budget, must have no open encumbrances, must have no asset, liability or fund balance balances other
563            // than object code 9899
564            // (9899 is fund balance for us), and the process of closing income and expense into 9899 must take the 9899 balance to
565            // zero.
566            if (balanceService.hasAssetLiabilityFundBalanceBalances(newAccount)) {
567                putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCOUNT_CLOSED_NO_FUND_BALANCES);
568                success &= false;
569            }
570    
571            // We must not have any pending labor ledger entries
572            if (SpringContext.getBean(LaborModuleService.class).hasPendingLaborLedgerEntry(newAccount.getChartOfAccountsCode(), newAccount.getAccountNumber())) {
573                putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCOUNT_CLOSED_PENDING_LABOR_LEDGER_ENTRIES);
574                success &= false;
575            }
576    
577            return success;
578        }
579    
580        /**
581         * This method checks to see if the account expiration date is today's date or earlier
582         * 
583         * @param newAccount
584         * @return fails if the expiration date is null or after today's date
585         */
586        protected boolean checkAccountExpirationDateValidTodayOrEarlier(Account newAccount) {
587    
588            // get today's date, with no time component
589            Date todaysDate = new Date(getDateTimeService().getCurrentDate().getTime());
590            todaysDate.setTime(DateUtils.truncate(todaysDate, Calendar.DAY_OF_MONTH).getTime());
591            // TODO: convert this to using Wes' Kuali DateUtils once we're using Date's instead of Timestamp
592            
593            // get the expiration date, if any
594            Date expirationDate = newAccount.getAccountExpirationDate();
595            if (ObjectUtils.isNull(expirationDate)) {
596                putFieldError("accountExpirationDate", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_CANNOT_BE_CLOSED_EXP_DATE_INVALID);
597                return false;
598            }
599    
600            // when closing an account, the account expiration date must be the current date or earlier
601            expirationDate.setTime(DateUtils.truncate(expirationDate, Calendar.DAY_OF_MONTH).getTime());
602            if (expirationDate.after(todaysDate)) {
603                putFieldError("accountExpirationDate", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_CANNOT_BE_CLOSED_EXP_DATE_INVALID);
604                return false;
605            }
606    
607            return true;
608        }
609    
610        /**
611         * This method checks to see if any Contracts and Grants business rules were violated Calls the following sub-rules:
612         * checkCgRequiredFields checkCgIncomeStreamRequired
613         * 
614         * @param maintenanceDocument
615         * @return false on rules violation
616         */
617        protected boolean checkContractsAndGrants(MaintenanceDocument maintenanceDocument) {
618    
619            LOG.info("checkContractsAndGrants called");
620    
621            boolean success = true;
622    
623            // Certain C&G fields are required if the Account belongs to the CG Fund Group
624            success &= checkCgRequiredFields(newAccount);
625    
626            // Income Stream account is required if this account is CG fund group,
627            // or GF (general fund) fund group (with some exceptions)
628            success &= checkIncomeStreamValid(newAccount);
629            
630            // check if the new account has a valid responsibility id
631            if (!ObjectUtils.isNull(newAccount)) {
632                final boolean hasValidAccountResponsibility = contractsAndGrantsModuleService.hasValidAccountReponsiblityIdIfNotNull(newAccount);
633                if (!hasValidAccountResponsibility) {
634                    success &= hasValidAccountResponsibility;
635                    putFieldError("contractsAndGrantsAccountResponsibilityId", KFSKeyConstants.ERROR_DOCUMENT_ACCTMAINT_INVALID_CG_RESPONSIBILITY , new String[] { newAccount.getContractsAndGrantsAccountResponsibilityId().toString(), newAccount.getChartOfAccountsCode(), newAccount.getAccountNumber() });
636                }
637            }
638    
639            return success;
640        }
641    
642        /**
643         * This method checks to see if the income stream account is required
644         * 
645         * @param newAccount
646         * @return fails if it is required and not entered, or not valid
647         */
648        protected boolean checkIncomeStreamValid(Account newAccount) {
649            // if the subFundGroup object is null, we can't test, so exit
650            if (ObjectUtils.isNull(newAccount.getSubFundGroup())) {
651                return true;
652            }
653            String subFundGroupCode = newAccount.getSubFundGroupCode().trim();
654            String fundGroupCode = newAccount.getSubFundGroup().getFundGroupCode().trim();
655            boolean valid = true;
656            if (getParameterService().getParameterEvaluator(Account.class, KFSConstants.ChartApcParms.INCOME_STREAM_ACCOUNT_REQUIRING_FUND_GROUPS, fundGroupCode).evaluationSucceeds()) {
657                if (getParameterService().getParameterEvaluator(Account.class, KFSConstants.ChartApcParms.INCOME_STREAM_ACCOUNT_REQUIRING_SUB_FUND_GROUPS, subFundGroupCode).evaluationSucceeds()) {
658                    if (StringUtils.isBlank(newAccount.getIncomeStreamFinancialCoaCode())) {
659                        putFieldError(KFSPropertyConstants.INCOME_STREAM_CHART_OF_ACCOUNTS_CODE, KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_INCOME_STREAM_ACCT_COA_CANNOT_BE_EMPTY, new String[] { getDdService().getAttributeLabel(FundGroup.class, KFSConstants.FUND_GROUP_CODE_PROPERTY_NAME), fundGroupCode, getDdService().getAttributeLabel(SubFundGroup.class, KFSConstants.SUB_FUND_GROUP_CODE_PROPERTY_NAME), subFundGroupCode });
660                        valid = false;
661                    } 
662                    if (StringUtils.isBlank(newAccount.getIncomeStreamAccountNumber())) {
663                        putFieldError(KFSPropertyConstants.INCOME_STREAM_ACCOUNT_NUMBER, KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_INCOME_STREAM_ACCT_NBR_CANNOT_BE_EMPTY, new String[] { getDdService().getAttributeLabel(FundGroup.class, KFSConstants.FUND_GROUP_CODE_PROPERTY_NAME), fundGroupCode, getDdService().getAttributeLabel(SubFundGroup.class, KFSConstants.SUB_FUND_GROUP_CODE_PROPERTY_NAME), subFundGroupCode});
664                        valid = false;
665                    }
666                }
667            }
668            if (valid && (StringUtils.isNotBlank(newAccount.getIncomeStreamFinancialCoaCode()) || StringUtils.isNotBlank(newAccount.getIncomeStreamAccountNumber()))) {
669                if(!(newAccount.getIncomeStreamAccountNumber().equals(newAccount.getAccountNumber()) && newAccount.getIncomeStreamFinancialCoaCode().equals(newAccount.getChartOfAccountsCode()))) {
670                    if (!super.getDictionaryValidationService().validateReferenceExists(newAccount, KFSPropertyConstants.INCOME_STREAM_ACCOUNT)) {
671                        putFieldError(KFSPropertyConstants.INCOME_STREAM_ACCOUNT_NUMBER, KFSKeyConstants.ERROR_EXISTENCE, new StringBuffer(getDdService().getAttributeLabel(SubFundGroup.class, KFSPropertyConstants.INCOME_STREAM_ACCOUNT_NUMBER)).append(": ").append(newAccount.getIncomeStreamFinancialCoaCode()).append("-").append(newAccount.getIncomeStreamAccountNumber()).toString());
672                        valid = false;
673                    }
674                }   
675            }
676            return valid;
677        }
678    
679        /**
680         * This method checks to make sure that if the contracts and grants fields are required they are entered correctly
681         * 
682         * @param newAccount
683         * @return
684         */
685        protected boolean checkCgRequiredFields(Account newAccount) {
686    
687            boolean result = true;
688    
689            // Certain C&G fields are required if the Account belongs to the CG Fund Group
690            if (ObjectUtils.isNotNull(newAccount.getSubFundGroup())) {
691                if (getSubFundGroupService().isForContractsAndGrants(newAccount.getSubFundGroup())) {
692                    result &= checkEmptyBOField("acctIndirectCostRcvyTypeCd", newAccount.getAcctIndirectCostRcvyTypeCd(), replaceTokens(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ICR_TYPE_CODE_CANNOT_BE_EMPTY));
693                    result &= checkEmptyBOField("financialIcrSeriesIdentifier", newAccount.getFinancialIcrSeriesIdentifier(), replaceTokens(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ICR_SERIES_IDENTIFIER_CANNOT_BE_EMPTY));
694    
695                    // Validation for financialIcrSeriesIdentifier
696                    if (checkEmptyBOField(KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, newAccount.getFinancialIcrSeriesIdentifier(), replaceTokens(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ICR_SERIES_IDENTIFIER_CANNOT_BE_EMPTY))) {
697                        String fiscalYear = StringUtils.EMPTY + SpringContext.getBean(UniversityDateService.class).getCurrentFiscalYear();
698                        String icrSeriesId = newAccount.getFinancialIcrSeriesIdentifier();
699                        
700                        Map<String, String> pkMap = new HashMap<String, String>();
701                        pkMap.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, fiscalYear);
702                        pkMap.put(KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, icrSeriesId);
703                        Collection<IndirectCostRecoveryRateDetail> icrRateDetails = getBoService().findMatching(IndirectCostRecoveryRateDetail.class, pkMap);
704                        
705                        if (ObjectUtils.isNull(icrRateDetails) || icrRateDetails.isEmpty()) {
706                            String label = SpringContext.getBean(DataDictionaryService.class).getAttributeLabel(Account.class, KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER);
707                            putFieldError(KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, KFSKeyConstants.ERROR_EXISTENCE, label + " (" + icrSeriesId + ")");
708                            result &= false;
709                        }
710                        else {
711                            for(IndirectCostRecoveryRateDetail icrRateDetail : icrRateDetails) {
712                                if(ObjectUtils.isNull(icrRateDetail.getIndirectCostRecoveryRate())){                                
713                                    putFieldError(KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, KFSKeyConstants.IndirectCostRecovery.ERROR_DOCUMENT_ICR_RATE_NOT_FOUND, new String[]{fiscalYear, icrSeriesId});
714                                    result &= false;
715                                    break;
716                                }
717                            }
718                        }
719                    }
720    
721                    result &= checkEmptyBOField("indirectCostRcvyFinCoaCode", newAccount.getIndirectCostRcvyFinCoaCode(), replaceTokens(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ICR_CHART_CODE_CANNOT_BE_EMPTY));
722                    result &= checkEmptyBOField("indirectCostRecoveryAcctNbr", newAccount.getIndirectCostRecoveryAcctNbr(), replaceTokens(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ICR_ACCOUNT_CANNOT_BE_EMPTY));
723                    result &= checkContractControlAccountNumberRequired(newAccount);
724                }
725                else {
726                    // this is not a C&G fund group. So users should not fill in any fields in the C&G tab.
727                    result &= checkCGFieldNotFilledIn(newAccount, "acctIndirectCostRcvyTypeCd");
728                    result &= checkCGFieldNotFilledIn(newAccount, "financialIcrSeriesIdentifier");
729                    result &= checkCGFieldNotFilledIn(newAccount, "indirectCostRcvyFinCoaCode");
730                    result &= checkCGFieldNotFilledIn(newAccount, "indirectCostRecoveryAcctNbr");
731                }
732            }
733            return result;
734        }
735    
736        /**
737         * This method is a helper method that replaces error tokens with values for contracts and grants labels
738         * 
739         * @param errorConstant
740         * @return error string that has had tokens "{0}" and "{1}" replaced
741         */
742        protected String replaceTokens(String errorConstant) {
743            String cngLabel = getSubFundGroupService().getContractsAndGrantsDenotingAttributeLabel();
744            String cngValue = getSubFundGroupService().getContractsAndGrantsDenotingValueForMessage();
745            String result = getKualiConfigurationService().getPropertyString(errorConstant);
746            result = StringUtils.replace(result, "{0}", cngLabel);
747            result = StringUtils.replace(result, "{1}", cngValue);
748            return result;
749        }
750    
751        /**
752         * This method checks to make sure that if the contract control account exists it is the same as the Account that we are working
753         * on
754         * 
755         * @param newAccount
756         * @return false if the contract control account is entered and is not the same as the account we are maintaining
757         */
758        protected boolean checkContractControlAccountNumberRequired(Account newAccount) {
759    
760            boolean result = true;
761    
762            // Contract Control account must either exist or be the same as account being maintained
763    
764            if (ObjectUtils.isNull(newAccount.getContractControlFinCoaCode())) {
765                return result;
766            }
767            if (ObjectUtils.isNull(newAccount.getContractControlAccountNumber())) {
768                return result;
769            }
770            if ((newAccount.getContractControlFinCoaCode().equals(newAccount.getChartOfAccountsCode())) && (newAccount.getContractControlAccountNumber().equals(newAccount.getAccountNumber()))) {
771                return result;
772            }
773    
774            // do an existence/active test
775            DictionaryValidationService dvService = super.getDictionaryValidationService();
776            boolean referenceExists = dvService.validateReferenceExists(newAccount, "contractControlAccount");
777            if (!referenceExists) {
778                putFieldError("contractControlAccountNumber", KFSKeyConstants.ERROR_EXISTENCE, "Contract Control Account: " + newAccount.getContractControlFinCoaCode() + "-" + newAccount.getContractControlAccountNumber());
779                result &= false;
780            }
781    
782            return result;
783        }
784    
785        /**
786         * This method checks to see if any expiration date field rules were violated
787         * 
788         * @param maintenanceDocument
789         * @return false on rules violation
790         */
791        protected boolean checkExpirationDate(MaintenanceDocument maintenanceDocument) {
792    
793            LOG.info("checkExpirationDate called");
794    
795            boolean success = true;
796    
797            Date oldExpDate = oldAccount.getAccountExpirationDate();
798            Date newExpDate = newAccount.getAccountExpirationDate();
799            Date today = new Date(getDateTimeService().getCurrentTimestamp().getTime());
800            today.setTime(DateUtils.truncate(today, Calendar.DAY_OF_MONTH).getTime()); // remove any time components
801    
802            // When updating an account expiration date, the date must be today or later
803            // Only run this test if this maintenance doc
804            // is an edit doc
805            if (isUpdatedExpirationDateInvalid(maintenanceDocument)) {
806                putFieldError("accountExpirationDate", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_EXP_DATE_TODAY_LATER);
807                success &= false;
808            }
809    
810            // a continuation account is required if the expiration date is completed.
811            if (ObjectUtils.isNotNull(newExpDate)) {
812                if (StringUtils.isBlank(newAccount.getContinuationAccountNumber())) {
813                    putFieldError("continuationAccountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_CONTINUATION_ACCT_REQD_IF_EXP_DATE_COMPLETED);
814                }
815                if (StringUtils.isBlank(newAccount.getContinuationFinChrtOfAcctCd())) {
816                    putFieldError("continuationFinChrtOfAcctCd", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_CONTINUATION_FINCODE_REQD_IF_EXP_DATE_COMPLETED);
817                    // putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_CONTINUATION_ACCT_REQD_IF_EXP_DATE_COMPLETED);
818                    success &= false;
819                }
820            }
821    
822            // If creating a new account if acct_expiration_dt is set then
823            // the acct_expiration_dt must be changed to a date that is today or later
824            if (maintenanceDocument.isNew() && ObjectUtils.isNotNull(newExpDate)) {
825                if (!newExpDate.after(today) && !newExpDate.equals(today)) {
826                    putFieldError("accountExpirationDate", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_EXP_DATE_TODAY_LATER);
827                    // putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_EXP_DATE_TODAY_LATER);
828                    success &= false;
829                }
830            }
831    
832            // acct_expiration_dt can not be before acct_effect_dt
833            Date effectiveDate = newAccount.getAccountEffectiveDate();
834            if (ObjectUtils.isNotNull(effectiveDate) && ObjectUtils.isNotNull(newExpDate)) {
835                if (newExpDate.before(effectiveDate)) {
836                    putFieldError("accountExpirationDate", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_EXP_DATE_CANNOT_BE_BEFORE_EFFECTIVE_DATE);
837                    // putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_EXP_DATE_CANNOT_BE_BEFORE_EFFECTIVE_DATE);
838                    success &= false;
839                }
840            }
841    
842            return success;
843        }
844    
845        /**
846         * This method checks to see if the new expiration date is different from the old expiration and if it has if it is invalid
847         * 
848         * @param maintDoc
849         * @return true if expiration date has changed and is invalid
850         */
851        protected boolean isUpdatedExpirationDateInvalid(MaintenanceDocument maintDoc) {
852    
853            // if this isn't an Edit document, we're not interested
854            if (!maintDoc.isEdit()) {
855                return false;
856            }
857    
858            Date oldExpDate = oldAccount.getAccountExpirationDate();
859            Date newExpDate = newAccount.getAccountExpirationDate();
860            Date today = new Date(getDateTimeService().getCurrentDate().getTime());
861            today.setTime(DateUtils.truncate(today, Calendar.DAY_OF_MONTH).getTime()); // remove any time components
862    
863            // When updating an account expiration date, the date must be today or later
864            // Only run this test if this maintenance doc
865            // is an edit doc
866            boolean expDateHasChanged = false;
867    
868            // if the old version of the account had no expiration date, and the new
869            // one has a date
870            if (ObjectUtils.isNull(oldExpDate) && ObjectUtils.isNotNull(newExpDate)) {
871                expDateHasChanged = true;
872            }
873    
874            // if there was an old and a new expDate, but they're different
875            else if (ObjectUtils.isNotNull(oldExpDate) && ObjectUtils.isNotNull(newExpDate)) {
876                if (!oldExpDate.equals(newExpDate)) {
877                    expDateHasChanged = true;
878                }
879            }
880    
881            // if the expiration date hasn't changed, we're not interested
882            if (!expDateHasChanged) {
883                return false;
884            }
885    
886            // make a shortcut to the newAccount
887            Account newAccount = (Account) maintDoc.getNewMaintainableObject().getBusinessObject();
888    
889            // expirationDate must be today or later than today (cannot be before today)
890            if (newExpDate.equals(today) || newExpDate.after(today)) {
891                return false;
892            }
893            else
894                return true;
895        }
896    
897        /**
898         * This method checks to see if any Fund Group rules were violated Specifically: if we are dealing with a "GF" (General Fund) we
899         * cannot have an account with a budget recording level of "M" (Mixed)
900         * 
901         * @param maintenanceDocument
902         * @return false on rules violation
903         */
904        protected boolean checkFundGroup(MaintenanceDocument maintenanceDocument) {
905    
906            LOG.info("checkFundGroup called");
907    
908            boolean success = true;
909            SubFundGroup subFundGroup = newAccount.getSubFundGroup();
910    
911            if (ObjectUtils.isNotNull(subFundGroup)) {
912    
913                // get values for fundGroupCode and restrictedStatusCode
914                String fundGroupCode = "";
915                String restrictedStatusCode = "";
916                if (StringUtils.isNotBlank(subFundGroup.getFundGroupCode())) {
917                    fundGroupCode = subFundGroup.getFundGroupCode().trim();
918                }
919                if (StringUtils.isNotBlank(newAccount.getAccountRestrictedStatusCode())) {
920                    restrictedStatusCode = newAccount.getAccountRestrictedStatusCode().trim();
921                }
922            }
923    
924            return success;
925        }
926    
927        /**
928         * This method checks to see if any SubFund Group rules were violated Specifically: if SubFundGroup is empty or not "PFCMR" we
929         * cannot have a campus code or building code if SubFundGroup is "PFCMR" then campus code and building code "must" be entered
930         * and be valid codes
931         * 
932         * @param maintenanceDocument
933         * @return false on rules violation
934         */
935        protected boolean checkSubFundGroup(MaintenanceDocument maintenanceDocument) {
936    
937            LOG.info("checkSubFundGroup called");
938    
939            boolean success = true;
940    
941            String subFundGroupCode = newAccount.getSubFundGroupCode();
942    
943            if (newAccount.getAccountDescription() != null) {
944    
945                String campusCode = newAccount.getAccountDescription().getCampusCode();
946                String buildingCode = newAccount.getAccountDescription().getBuildingCode();
947    
948                // check if sub fund group code is blank
949                if (StringUtils.isBlank(subFundGroupCode)) {
950    
951                    // check if campus code and building code are NOT blank
952                    if (!StringUtils.isBlank(campusCode) || !StringUtils.isBlank(buildingCode)) {
953    
954                        // if sub_fund_grp_cd is blank, campus code should NOT be entered
955                        if (!StringUtils.isBlank(campusCode)) {
956                            putFieldError("accountDescription.campusCode", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_BLANK_SUBFUNDGROUP_WITH_CAMPUS_CD_FOR_BLDG, subFundGroupCode);
957                            success &= false;
958                        }
959    
960                        // if sub_fund_grp_cd is blank, then bldg_cd should NOT be entered
961                        if (!StringUtils.isBlank(buildingCode)) {
962                            putFieldError("accountDescription.buildingCode", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_BLANK_SUBFUNDGROUP_WITH_BUILDING_CD, subFundGroupCode);
963                            success &= false;
964                        }
965    
966                    }
967                    else {
968    
969                        // if all sub fund group, campus code, building code are all blank return true
970                        return success;
971                    }
972    
973                }
974                else if (!StringUtils.isBlank(subFundGroupCode) && !ObjectUtils.isNull(newAccount.getSubFundGroup())) {
975    
976                    // Attempt to get the right SubFundGroup code to check the following logic with. If the value isn't available, go
977                    // ahead
978                    // and die, as this indicates a mis-configured application, and important business rules wont be implemented without it.
979                    ParameterEvaluator evaluator = getParameterService().getParameterEvaluator(Account.class, ACCT_CAPITAL_SUBFUNDGROUP, subFundGroupCode.trim());
980    
981                    if (evaluator.evaluationSucceeds()) {
982    
983                        // if sub_fund_grp_cd is 'PFCMR' then campus_cd must be entered
984                        if (StringUtils.isBlank(campusCode)) {
985                            putFieldError("accountDescription.campusCode", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_CAMS_SUBFUNDGROUP_WITH_MISSING_CAMPUS_CD_FOR_BLDG, subFundGroupCode);
986                            success &= false;
987                        }
988    
989                        // if sub_fund_grp_cd is 'PFCMR' then bldg_cd must be entered
990                        if (StringUtils.isBlank(buildingCode)) {
991                            putFieldError("accountDescription.buildingCode", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_CAMS_SUBFUNDGROUP_WITH_MISSING_BUILDING_CD, subFundGroupCode);
992                            success &= false;
993                        }
994    
995                        // the building object (campusCode & buildingCode) must exist in the DB
996                        if (!StringUtils.isBlank(campusCode) && !StringUtils.isBlank(buildingCode)) {
997    
998                            // make sure that primary key fields are upper case
999                            DataDictionaryService dds = getDdService();
1000                            Boolean buildingCodeForceUppercase = dds.getAttributeForceUppercase(AccountDescription.class, KFSPropertyConstants.BUILDING_CODE);
1001                            if (StringUtils.isNotBlank(buildingCode) && buildingCodeForceUppercase != null && buildingCodeForceUppercase.booleanValue() == true) {
1002                                buildingCode = buildingCode.toUpperCase();
1003                            }
1004    
1005                            Boolean campusCodeForceUppercase = dds.getAttributeForceUppercase(AccountDescription.class, KFSPropertyConstants.CAMPUS_CODE);
1006                            if (StringUtils.isNotBlank(campusCode) && campusCodeForceUppercase != null && campusCodeForceUppercase.booleanValue() == true) {
1007                                campusCode = campusCode.toUpperCase();
1008                            }
1009    
1010                            Map<String, String> pkMap = new HashMap<String, String>();
1011                            pkMap.put("campusCode", campusCode);
1012                            pkMap.put("buildingCode", buildingCode);
1013    
1014                            Building building = (Building) getBoService().findByPrimaryKey(Building.class, pkMap);
1015                            if (building == null) {
1016                                putFieldError("accountDescription.campusCode", KFSKeyConstants.ERROR_EXISTENCE, campusCode);
1017                                putFieldError("accountDescription.buildingCode", KFSKeyConstants.ERROR_EXISTENCE, buildingCode);
1018                                success &= false;
1019                            }
1020                        }
1021                    }
1022                    else {
1023    
1024                        // if sub_fund_grp_cd is NOT 'PFCMR', campus code should NOT be entered
1025                        if (!StringUtils.isBlank(campusCode)) {
1026                            putFieldError("accountDescription.campusCode", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_NONCAMS_SUBFUNDGROUP_WITH_CAMPUS_CD_FOR_BLDG, subFundGroupCode);
1027                            success &= false;
1028                        }
1029    
1030                        // if sub_fund_grp_cd is NOT 'PFCMR' then bldg_cd should NOT be entered
1031                        if (!StringUtils.isBlank(buildingCode)) {
1032                            putFieldError("accountDescription.buildingCode", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_NONCAMS_SUBFUNDGROUP_WITH_BUILDING_CD, subFundGroupCode);
1033                            success &= false;
1034                        }
1035                    }
1036                }
1037    
1038            }
1039    
1040            return success;
1041        }
1042    
1043        /**
1044         * the income stream account is required if account's sub fund group code's fund group code is either GF or CG.
1045         * 
1046         * @param newAccount
1047         * @return true if fund group code (obtained through sub fund group) is in the system parameter INCOME_STREAM_ACCOUNT_REQUIRING_FUND_GROUPS (values GF;CG)
1048         * else return false.
1049         */
1050        protected boolean checkIncomeStreamAccountRule() {
1051            // KFSMI-4877: if fund group is in system parameter values then income stream account number must exist.
1052            if ( ObjectUtils.isNotNull(newAccount.getSubFundGroup()) && StringUtils.isNotBlank(newAccount.getSubFundGroup().getFundGroupCode())) {
1053                if (ObjectUtils.isNull(newAccount.getIncomeStreamAccount())) {
1054                    String incomeStreamRequiringFundGroupCode = SpringContext.getBean(ParameterService.class).getParameterValue(Account.class, KFSConstants.ChartApcParms.INCOME_STREAM_ACCOUNT_REQUIRING_FUND_GROUPS);
1055                    if (StringUtils.containsIgnoreCase(newAccount.getSubFundGroup().getFundGroupCode(), incomeStreamRequiringFundGroupCode)) {
1056                        GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_NUMBER, KFSKeyConstants.ERROR_DOCUMENT_BA_NO_INCOME_STREAM_ACCOUNT, newAccount.getAccountNumber());
1057                        return false;
1058                    }
1059                }
1060            }
1061            return true;
1062        }
1063        
1064        /**
1065         * This method checks to see if the contracts and grants fields are filled in or not
1066         * 
1067         * @param account
1068         * @param propertyName - property to attach error to
1069         * @return false if the contracts and grants fields are blank
1070         */
1071        protected boolean checkCGFieldNotFilledIn(Account account, String propertyName) {
1072            boolean success = true;
1073            Object value = ObjectUtils.getPropertyValue(account, propertyName);
1074            if ((value instanceof String && !StringUtils.isBlank(value.toString())) || (value != null)) {
1075                success = false;
1076                putFieldError(propertyName, KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_CG_FIELDS_FILLED_FOR_NON_CG_ACCOUNT, new String[] { account.getSubFundGroupCode() });
1077            }
1078    
1079            return success;
1080        }
1081    
1082        /**
1083         * This method checks to see if account is allowed to cross chart; 
1084         * and if not makes sure that the account number is unique in the whole system. 
1085         * This checking is only needed when adding a new account, 
1086         * since users are not allowed to change account numbers on editing.
1087         * 
1088         * @param maintenanceDocument
1089         * @return false on account-cross-chart rule violation
1090         */
1091        protected boolean checkUniqueAccountNumber(MaintenanceDocument maintenanceDocument) {
1092            boolean success = true;
1093            String accountNumber = newAccount.getAccountNumber();
1094            
1095            if (maintenanceDocument.isNew() && // if adding a new account
1096                    // while account is not allowed to cross chart 
1097                    !accountService.accountsCanCrossCharts() &&
1098                    // and with an account number that already exists
1099                    !accountService.getAccountsForAccountNumber(accountNumber).isEmpty()) {
1100                // report error
1101                success = false;
1102                putFieldError("accountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_NMBR_NOT_UNIQUE, accountNumber);            
1103            }
1104            
1105            return success;
1106        }
1107        
1108        /**
1109         * This method sets the generalLedgerPendingEntryService
1110         * 
1111         * @param generalLedgerPendingEntryService
1112         */
1113        public void setGeneralLedgerPendingEntryService(GeneralLedgerPendingEntryService generalLedgerPendingEntryService) {
1114            this.generalLedgerPendingEntryService = generalLedgerPendingEntryService;
1115        }
1116    
1117        /**
1118         * This method sets the balanceService
1119         * 
1120         * @param balanceService
1121         */
1122        public void setBalanceService(BalanceService balanceService) {
1123            this.balanceService = balanceService;
1124        }
1125    
1126        /**
1127         * Sets the accountService attribute value.
1128         * 
1129         * @param accountService The accountService to set.
1130         */
1131        public final void setAccountService(AccountService accountService) {
1132            this.accountService = accountService;
1133        }
1134    
1135        /**
1136         * Sets the contractsAndGrantsModuleService attribute value.
1137         * @param contractsAndGrantsModuleService The contractsAndGrantsModuleService to set.
1138         */
1139        public void setContractsAndGrantsModuleService(ContractsAndGrantsModuleService contractsAndGrantsModuleService) {
1140            this.contractsAndGrantsModuleService = contractsAndGrantsModuleService;
1141        }
1142    
1143        public SubFundGroupService getSubFundGroupService() {
1144            if ( subFundGroupService == null ) {
1145                subFundGroupService = SpringContext.getBean(SubFundGroupService.class);
1146            }
1147            return subFundGroupService;
1148        }
1149    
1150        public ParameterService getParameterService() {
1151            if ( parameterService == null ) {
1152                parameterService = SpringContext.getBean(ParameterService.class);
1153            }
1154            return parameterService;
1155        }
1156    
1157    }
1158