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.HashMap;
022    import java.util.HashSet;
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.AccountGlobal;
030    import org.kuali.kfs.coa.businessobject.AccountGlobalDetail;
031    import org.kuali.kfs.coa.businessobject.SubFundGroup;
032    import org.kuali.kfs.coa.service.OrganizationService;
033    import org.kuali.kfs.coa.service.SubFundGroupService;
034    import org.kuali.kfs.sys.KFSConstants;
035    import org.kuali.kfs.sys.KFSKeyConstants;
036    import org.kuali.kfs.sys.context.SpringContext;
037    import org.kuali.rice.kim.bo.Person;
038    import org.kuali.rice.kns.bo.PersistableBusinessObject;
039    import org.kuali.rice.kns.document.MaintenanceDocument;
040    import org.kuali.rice.kns.service.BusinessObjectService;
041    import org.kuali.rice.kns.service.DictionaryValidationService;
042    import org.kuali.rice.kns.util.GlobalVariables;
043    import org.kuali.rice.kns.util.ObjectUtils;
044    
045    /**
046     * This class represents the business rules for the maintenance of {@link AccountGlobal} business objects
047     */
048    public class AccountGlobalRule extends GlobalDocumentRuleBase {
049        protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AccountGlobalRule.class);
050    
051        protected static final String GENERAL_FUND_CD = "GF";
052        protected static final String RESTRICTED_FUND_CD = "RF";
053        protected static final String ENDOWMENT_FUND_CD = "EN";
054        protected static final String PLANT_FUND_CD = "PF";
055    
056        protected static final String RESTRICTED_CD_RESTRICTED = "R";
057        protected static final String RESTRICTED_CD_UNRESTRICTED = "U";
058        protected static final String RESTRICTED_CD_TEMPORARILY_RESTRICTED = "T";
059    
060        protected static final String SUB_FUND_GROUP_MEDICAL_PRACTICE_FUNDS = "MPRACT";
061    
062        protected AccountGlobal newAccountGlobal;
063        protected Timestamp today;
064    
065        public AccountGlobalRule() {
066            super();
067        }
068    
069        /**
070         * This method sets the convenience objects like newAccountGlobal and oldAccount, so you have short and easy handles to the new
071         * and old objects contained in the maintenance document. It also calls the BusinessObjectBase.refresh(), which will attempt to
072         * load all sub-objects from the DB by their primary keys, if available.
073         */
074        @Override
075        public void setupConvenienceObjects() {
076    
077            // setup newDelegateGlobal convenience objects, make sure all possible sub-objects are populated
078            newAccountGlobal = (AccountGlobal) super.getNewBo();
079            today = getDateTimeService().getCurrentTimestamp();
080            today.setTime(DateUtils.truncate(today, Calendar.DAY_OF_MONTH).getTime()); // remove any time components
081        }
082    
083        /**
084         * This method checks the following rules: checkEmptyValues checkGeneralRules checkContractsAndGrants checkExpirationDate
085         * checkOnlyOneChartErrorWrapper checkFiscalOfficerIsValidKualiUser but does not fail if any of them fail (this only happens on
086         * routing)
087         * 
088         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
089         */
090        protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) {
091    
092            LOG.info("processCustomSaveDocumentBusinessRules called");
093            setupConvenienceObjects();
094    
095            checkEmptyValues();
096            checkGeneralRules(document);
097            checkOrganizationValidity(newAccountGlobal);
098            checkContractsAndGrants();
099            checkExpirationDate(document);
100            checkOnlyOneChartErrorWrapper(newAccountGlobal.getAccountGlobalDetails());
101            // checkFundGroup(document);
102            // checkSubFundGroup(document);
103    
104            // Save always succeeds, even if there are business rule failures
105            return true;
106        }
107    
108        /**
109         * This method checks the following rules: checkEmptyValues checkGeneralRules checkContractsAndGrants checkExpirationDate
110         * checkOnlyOneChartErrorWrapper checkFiscalOfficerIsValidKualiUser but does fail if any of these rule checks fail
111         * 
112         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomRouteDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
113         */
114        protected boolean processCustomRouteDocumentBusinessRules(MaintenanceDocument document) {
115    
116            LOG.info("processCustomRouteDocumentBusinessRules called");
117            setupConvenienceObjects();
118    
119            // default to success
120            boolean success = true;
121    
122            success &= checkEmptyValues();
123            success &= checkGeneralRules(document);
124            success &= checkContractsAndGrants();
125            success &= checkExpirationDate(document);
126            success &= checkAccountDetails(document, newAccountGlobal.getAccountGlobalDetails());
127            // success &= checkFundGroup(document);
128            // success &= checkSubFundGroup(document);
129    
130            return success;
131        }
132    
133        /**
134         * This method loops through the list of {@link AccountGlobalDetail}s and passes them off to checkAccountDetails for further
135         * rule analysis One rule it does check is checkOnlyOneChartErrorWrapper
136         * 
137         * @param document
138         * @param details
139         * @return true if the collection of {@link AccountGlobalDetail}s passes the sub-rules
140         */
141        public boolean checkAccountDetails(MaintenanceDocument document, List<AccountGlobalDetail> details) {
142            boolean success = true;
143    
144            // check if there are any accounts
145            if (details.size() == 0) {
146    
147                putFieldError(KFSConstants.MAINTENANCE_ADD_PREFIX + "accountGlobalDetails.accountNumber", KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_ACCOUNT_NO_ACCOUNTS);
148    
149                success = false;
150            }
151            else {
152                // check each account
153                int index = 0;
154                for (AccountGlobalDetail dtl : details) {
155                    String errorPath = MAINTAINABLE_ERROR_PREFIX + "accountGlobalDetails[" + index + "]";
156                    GlobalVariables.getMessageMap().addToErrorPath(errorPath);
157                    success &= checkAccountDetails(dtl);
158                    GlobalVariables.getMessageMap().removeFromErrorPath(errorPath);
159                    index++;
160                }
161                success &= checkOnlyOneChartErrorWrapper(details);
162            }
163    
164            return success;
165        }
166    
167        /**
168         * This method ensures that each {@link AccountGlobalDetail} is valid and has a valid account number
169         * 
170         * @param dtl
171         * @return true if the detail object contains a valid account
172         */
173        public boolean checkAccountDetails(AccountGlobalDetail dtl) {
174            boolean success = true;
175            int originalErrorCount = GlobalVariables.getMessageMap().getErrorCount();
176            getDictionaryValidationService().validateBusinessObject(dtl);
177            if (StringUtils.isNotBlank(dtl.getAccountNumber()) && StringUtils.isNotBlank(dtl.getChartOfAccountsCode())) {
178                dtl.refreshReferenceObject("account");
179                if (ObjectUtils.isNull(dtl.getAccount())) {
180                    GlobalVariables.getMessageMap().putError("accountNumber", KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_ACCOUNT_INVALID_ACCOUNT, new String[] { dtl.getChartOfAccountsCode(), dtl.getAccountNumber() });
181                }
182            }
183            success &= GlobalVariables.getMessageMap().getErrorCount() == originalErrorCount;
184    
185            return success;
186        }
187    
188        /**
189         * This method checks the basic rules for empty reference key values on a continuation account and an income stream account
190         * 
191         * @return true if no empty values or partially filled out reference keys
192         */
193        protected boolean checkEmptyValues() {
194    
195            LOG.info("checkEmptyValues called");
196    
197            boolean success = true;
198    
199            // this set confirms that all fields which are grouped (ie, foreign keys of a referenc
200            // object), must either be none filled out, or all filled out.
201            success &= checkForPartiallyFilledOutReferenceForeignKeys("continuationAccount");
202            success &= checkForPartiallyFilledOutReferenceForeignKeys("incomeStreamAccount");
203    
204            return success;
205        }
206    
207        /**
208         * This method checks some of the general business rules associated with this document Such as: valid user for fiscal officer,
209         * supervisor or account manager (and not the same individual) are they trying to use an expired continuation account
210         * 
211         * @param maintenanceDocument
212         * @return false on rules violation
213         */
214        protected boolean checkGeneralRules(MaintenanceDocument maintenanceDocument) {
215    
216            LOG.info("checkGeneralRules called");
217            Person fiscalOfficer = newAccountGlobal.getAccountFiscalOfficerUser();
218            Person accountManager = newAccountGlobal.getAccountManagerUser();
219            Person accountSupervisor = newAccountGlobal.getAccountSupervisoryUser();
220    
221            boolean success = true;
222    
223            if (!StringUtils.isBlank(newAccountGlobal.getAccountFiscalOfficerSystemIdentifier()) && (ObjectUtils.isNull(fiscalOfficer) || StringUtils.isBlank(fiscalOfficer.getPrincipalId()) || !getDocumentHelperService().getDocumentAuthorizer(maintenanceDocument).isAuthorized(maintenanceDocument, KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_FISCAL_OFFICER, fiscalOfficer.getPrincipalId()))) {
224                final String fiscalOfficerName = fiscalOfficer != null ? fiscalOfficer.getName() : newAccountGlobal.getAccountFiscalOfficerSystemIdentifier();
225                super.putFieldError("accountFiscalOfficerUser.principalName", KFSKeyConstants.ERROR_USER_MISSING_PERMISSION, new String[] {fiscalOfficerName, KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_FISCAL_OFFICER});
226                            success = false;
227            }
228            if (!StringUtils.isBlank(newAccountGlobal.getAccountsSupervisorySystemsIdentifier()) && (ObjectUtils.isNull(accountSupervisor) || StringUtils.isBlank(accountSupervisor.getPrincipalId()) || !getDocumentHelperService().getDocumentAuthorizer(maintenanceDocument).isAuthorized(maintenanceDocument, KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_ACCOUNT_SUPERVISOR, accountSupervisor.getPrincipalId()))) {
229                final String accountSupervisorName = accountSupervisor != null ? accountSupervisor.getName() : newAccountGlobal.getAccountsSupervisorySystemsIdentifier();
230                super.putFieldError("accountSupervisoryUser.principalName", KFSKeyConstants.ERROR_USER_MISSING_PERMISSION, new String[] {accountSupervisorName, KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_ACCOUNT_SUPERVISOR});
231                            success = false;
232            }
233            if (!StringUtils.isBlank(newAccountGlobal.getAccountManagerSystemIdentifier()) && (ObjectUtils.isNull(accountManager) || StringUtils.isBlank(accountManager.getPrincipalId()) || !getDocumentHelperService().getDocumentAuthorizer(maintenanceDocument).isAuthorized(maintenanceDocument, KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_ACCOUNT_MANAGER, accountManager.getPrincipalId()))) {
234                final String accountManagerName = accountManager != null ? accountManager.getName() : newAccountGlobal.getAccountManagerSystemIdentifier();
235                super.putFieldError("accountManagerUser.principalName", KFSKeyConstants.ERROR_USER_MISSING_PERMISSION, new String[] {accountManagerName, KFSConstants.ParameterNamespaces.CHART, KFSConstants.PermissionNames.SERVE_AS_ACCOUNT_MANAGER});
236                            success = false;
237            }
238    
239            // the supervisor cannot be the same as the fiscal officer or account manager.
240            if (isSupervisorSameAsFiscalOfficer(newAccountGlobal)) {
241                success &= false;
242                putFieldError("accountsSupervisorySystemsIdentifier", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_SUPER_CANNOT_BE_FISCAL_OFFICER);
243            }
244            if (isSupervisorSameAsManager(newAccountGlobal)) {
245                success &= false;
246                putFieldError("accountManagerSystemIdentifier", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_SUPER_CANNOT_BE_ACCT_MGR);
247            }
248    
249            // disallow continuation account being expired
250            if (isContinuationAccountExpired(newAccountGlobal)) {
251                success &= false;
252                putFieldError("continuationAccountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCOUNT_EXPIRED_CONTINUATION);
253            }
254    
255            // loop over change detail objects to test if the supervisor/FO/mgr restrictions are in place
256            // only need to do this check if the entered information does not already violate the rules
257            if (!isSupervisorSameAsFiscalOfficer(newAccountGlobal) && !isSupervisorSameAsManager(newAccountGlobal)) {
258                success &= checkAllAccountUsers(newAccountGlobal, fiscalOfficer, accountManager, accountSupervisor);
259            }
260    
261            return success;
262        }
263    
264        /**
265         * This method checks to make sure that if the users are filled out (fiscal officer, supervisor, manager) that they are not the
266         * same individual Only need to check this if these are new users that override existing users on the {@link Account} object
267         * 
268         * @param doc
269         * @param newFiscalOfficer
270         * @param newManager
271         * @param newSupervisor
272         * @return true if the users are either not changed or pass the sub-rules
273         */
274        protected boolean checkAllAccountUsers(AccountGlobal doc, Person newFiscalOfficer, Person newManager, Person newSupervisor) {
275            boolean success = true;
276    
277            if (LOG.isDebugEnabled()) {
278                LOG.debug("newSupervisor: " + newSupervisor);
279                LOG.debug("newFiscalOfficer: " + newFiscalOfficer);
280                LOG.debug("newManager: " + newManager);
281            }
282            // only need to do this check if at least one of the user fields is
283            // non null
284            if (newSupervisor != null || newFiscalOfficer != null || newManager != null) {
285                // loop over all AccountGlobalDetail records
286                int index = 0;
287                for (AccountGlobalDetail detail : doc.getAccountGlobalDetails()) {
288                    success &= checkAccountUsers(detail, newFiscalOfficer, newManager, newSupervisor, index);
289                    index++;
290                }
291            }
292    
293            return success;
294        }
295    
296        /**
297         * This method checks that the new users (fiscal officer, supervisor, manager) are not the same individual for the
298         * {@link Account} being changed (contained in the {@link AccountGlobalDetail})
299         * 
300         * @param detail - where the Account information is stored
301         * @param newFiscalOfficer
302         * @param newManager
303         * @param newSupervisor
304         * @param index - for storing the error line
305         * @return true if the new users pass this sub-rule
306         */
307        protected boolean checkAccountUsers(AccountGlobalDetail detail, Person newFiscalOfficer, Person newManager, Person newSupervisor, int index) {
308            boolean success = true;
309    
310            // only need to do this check if at least one of the user fields is non null
311            if (newSupervisor != null || newFiscalOfficer != null || newManager != null) {
312                // loop over all AccountGlobalDetail records
313                detail.refreshReferenceObject("account");
314                Account account = detail.getAccount();
315                if (ObjectUtils.isNotNull(account)){
316                    if (LOG.isDebugEnabled()) {
317                        LOG.debug("old-Supervisor: " + account.getAccountSupervisoryUser());
318                        LOG.debug("old-FiscalOfficer: " + account.getAccountFiscalOfficerUser());
319                        LOG.debug("old-Manager: " + account.getAccountManagerUser());
320                    }
321                    // only need to check if they are not being overridden by the change document
322                    if (newSupervisor != null && newSupervisor.getPrincipalId() != null) {
323                        if (areTwoUsersTheSame(newSupervisor, account.getAccountFiscalOfficerUser())) {
324                            success = false;
325                            putFieldError("accountGlobalDetails[" + index + "].accountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_SUPER_CANNOT_EQUAL_EXISTING_FISCAL_OFFICER, new String[] { account.getAccountFiscalOfficerUser().getPrincipalName(), "Fiscal Officer", detail.getAccountNumber() });
326                        }
327                        if (areTwoUsersTheSame(newSupervisor, account.getAccountManagerUser())) {
328                            success = false;
329                            putFieldError("accountGlobalDetails[" + index + "].accountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_SUPER_CANNOT_EQUAL_EXISTING_ACCT_MGR, new String[] { account.getAccountManagerUser().getPrincipalName(), "Account Manager", detail.getAccountNumber() });
330                        }
331                    }
332                    if (newManager != null && newManager.getPrincipalId() != null) {
333                        if (areTwoUsersTheSame(newManager, account.getAccountSupervisoryUser())) {
334                            success = false;
335                            putFieldError("accountGlobalDetails[" + index + "].accountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_MGR_CANNOT_EQUAL_EXISTING_ACCT_SUPERVISOR, new String[] { account.getAccountSupervisoryUser().getPrincipalName(), "Account Supervisor", detail.getAccountNumber() });
336                        }
337                    }
338                    if (newFiscalOfficer != null && newFiscalOfficer.getPrincipalId() != null) {
339                        if (areTwoUsersTheSame(newFiscalOfficer, account.getAccountSupervisoryUser())) {
340                            success = false;
341                            putFieldError("accountGlobalDetails[" + index + "].accountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_FISCAL_OFFICER_CANNOT_EQUAL_EXISTING_ACCT_SUPERVISOR, new String[] { account.getAccountSupervisoryUser().getPrincipalName(), "Account Supervisor", detail.getAccountNumber() });
342                        }
343                    }
344                }
345                else {
346                    LOG.warn("AccountGlobalDetail object has null account object:" + detail.getChartOfAccountsCode() + "-" + detail.getAccountNumber());
347                }
348            }
349    
350            return success;
351        }
352    
353        /**
354         * This method is a helper method for checking if the supervisor user is the same as the fiscal officer Calls
355         * {@link AccountGlobalRule#areTwoUsersTheSame(Person, Person)}
356         * 
357         * @param accountGlobals
358         * @return true if the two users are the same
359         */
360        protected boolean isSupervisorSameAsFiscalOfficer(AccountGlobal accountGlobals) {
361            return areTwoUsersTheSame(accountGlobals.getAccountSupervisoryUser(), accountGlobals.getAccountFiscalOfficerUser());
362        }
363    
364        /**
365         * This method is a helper method for checking if the supervisor user is the same as the manager Calls
366         * {@link AccountGlobalRule#areTwoUsersTheSame(Person, Person)}
367         * 
368         * @param accountGlobals
369         * @return true if the two users are the same
370         */
371        protected boolean isSupervisorSameAsManager(AccountGlobal accountGlobals) {
372            return areTwoUsersTheSame(accountGlobals.getAccountSupervisoryUser(), accountGlobals.getAccountManagerUser());
373        }
374    
375        /**
376         * This method checks to see if two users are the same Person using their identifiers
377         * 
378         * @param user1
379         * @param user2
380         * @return true if these two users are the same
381         */
382        protected boolean areTwoUsersTheSame(Person user1, Person user2) {
383            if (ObjectUtils.isNull(user1) || user1.getPrincipalId() == null ) {
384                return false;
385            }
386            if (ObjectUtils.isNull(user2) || user2.getPrincipalId() == null ) {
387                return false;
388            }
389            return user1.getPrincipalId().equals(user2.getPrincipalId());
390        }
391    
392        /**
393         * This method checks to see if any expiration date field rules were violated Loops through each detail object and calls
394         * {@link AccountGlobalRule#checkExpirationDate(MaintenanceDocument, AccountGlobalDetail)}
395         * 
396         * @param maintenanceDocument
397         * @return false on rules violation
398         */
399        protected boolean checkExpirationDate(MaintenanceDocument maintenanceDocument) {
400            LOG.info("checkExpirationDate called");
401    
402            boolean success = true;
403            Date newExpDate = newAccountGlobal.getAccountExpirationDate();
404    
405            // If creating a new account if acct_expiration_dt is set and the fund_group is not "CG" then
406            // the acct_expiration_dt must be changed to a date that is today or later
407            if (ObjectUtils.isNotNull(newExpDate)) {
408                if (ObjectUtils.isNotNull(newAccountGlobal.getSubFundGroup())) {
409                    if (!SpringContext.getBean(SubFundGroupService.class).isForContractsAndGrants(newAccountGlobal.getSubFundGroup())) {
410                        if (!newExpDate.after(today) && !newExpDate.equals(today)) {
411                            putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_EXP_DATE_TODAY_LATER);
412                            success &= false;
413                        }
414                    }
415                }
416            }
417    
418            // a continuation account is required if the expiration date is completed.
419            success &= checkContinuationAccount(maintenanceDocument, newExpDate);
420    
421            for (AccountGlobalDetail detail : newAccountGlobal.getAccountGlobalDetails()) {
422                success &= checkExpirationDate(maintenanceDocument, detail);
423            }
424            return success;
425        }
426    
427        /**
428         * This method checks to see if any expiration date field rules were violated in relation to the given detail record
429         * 
430         * @param maintenanceDocument
431         * @param detail - the account detail we are investigating
432         * @return false on rules violation
433         */
434        protected boolean checkExpirationDate(MaintenanceDocument maintenanceDocument, AccountGlobalDetail detail) {
435            boolean success = true;
436            Date newExpDate = newAccountGlobal.getAccountExpirationDate();
437    
438            // load the object by keys
439            Account account = (Account) SpringContext.getBean(BusinessObjectService.class).findByPrimaryKey(Account.class, detail.getPrimaryKeys());
440            if (ObjectUtils.isNotNull(account)) {
441                Date oldExpDate = account.getAccountExpirationDate();
442    
443                // When updating an account expiration date, the date must be today or later
444                // (except for C&G accounts). Only run this test if this maint doc
445                // is an edit doc
446                if (isUpdatedExpirationDateInvalid(account, newAccountGlobal)) {
447                    putFieldError("accountExpirationDate", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_EXP_DATE_TODAY_LATER);
448                    success &= false;
449                }
450    
451                // If creating a new account if acct_expiration_dt is set and the fund_group is not "CG" then
452                // the acct_expiration_dt must be changed to a date that is today or later
453                if (ObjectUtils.isNotNull(newExpDate) && ObjectUtils.isNull(newAccountGlobal.getSubFundGroup())) {
454                    if (ObjectUtils.isNotNull(account.getSubFundGroup())) {
455                        if (!account.isForContractsAndGrants()) {
456                            if (!newExpDate.after(today) && !newExpDate.equals(today)) {
457                                putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_EXP_DATE_TODAY_LATER);
458                                success &= false;
459                            }
460                        }
461                    }
462                }
463                // acct_expiration_dt can not be before acct_effect_dt
464                Date effectiveDate = account.getAccountEffectiveDate();
465                if (ObjectUtils.isNotNull(effectiveDate) && ObjectUtils.isNotNull(newExpDate)) {
466                    if (newExpDate.before(effectiveDate)) {
467                        putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_EXP_DATE_CANNOT_BE_BEFORE_EFFECTIVE_DATE);
468                        success &= false;
469                    }
470                }
471            }
472    
473            return success;
474        }
475    
476        /*
477         * protected boolean checkAccountExpirationDateValidTodayOrEarlier(Account newAccount) { // get today's date, with no time
478         * component Timestamp todaysDate = getDateTimeService().getCurrentTimestamp();
479         * todaysDate.setTime(DateUtils.truncate(todaysDate, Calendar.DAY_OF_MONTH).getTime()); // TODO: convert this to using Wes'
480         * kuali DateUtils once we're using Date's instead of Timestamp // get the expiration date, if any Timestamp expirationDate =
481         * newAccount.getAccountExpirationDate(); if (ObjectUtils.isNull(expirationDate)) { putFieldError("accountExpirationDate",
482         * KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_CANNOT_BE_CLOSED_EXP_DATE_INVALID); return false; } // when closing an account,
483         * the account expiration date must be the current date or earlier expirationDate.setTime(DateUtils.truncate(expirationDate,
484         * Calendar.DAY_OF_MONTH).getTime()); if (expirationDate.after(todaysDate)) { putFieldError("accountExpirationDate",
485         * KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_ACCT_CANNOT_BE_CLOSED_EXP_DATE_INVALID); return false; } return true; }
486         */
487    
488        /**
489         * This method checks to see if the updated expiration is not a valid one Only gets checked for specific {@link SubFundGroup}s
490         * 
491         * @param oldAccount
492         * @param newAccountGlobal
493         * @return true if date has changed and is invalid
494         */
495        protected boolean isUpdatedExpirationDateInvalid(Account oldAccount, AccountGlobal newAccountGlobal) {
496    
497            Date oldExpDate = oldAccount.getAccountExpirationDate();
498            Date newExpDate = newAccountGlobal.getAccountExpirationDate();
499    
500            // When updating an account expiration date, the date must be today or later
501            // (except for C&G accounts). Only run this test if this maint doc
502            // is an edit doc
503            boolean expDateHasChanged = false;
504    
505            // if the old version of the account had no expiration date, and the new
506            // one has a date
507            if (ObjectUtils.isNull(oldExpDate) && ObjectUtils.isNotNull(newExpDate)) {
508                expDateHasChanged = true;
509            }
510    
511            // if there was an old and a new expDate, but they're different
512            else if (ObjectUtils.isNotNull(oldExpDate) && ObjectUtils.isNotNull(newExpDate)) {
513                if (!oldExpDate.equals(newExpDate)) {
514                    expDateHasChanged = true;
515                }
516            }
517    
518            // if the expiration date hasnt changed, we're not interested
519            if (!expDateHasChanged) {
520                return false;
521            }
522    
523            // if a subFundGroup isnt present, we cannot continue the testing
524            SubFundGroup subFundGroup = newAccountGlobal.getSubFundGroup();
525            if (ObjectUtils.isNull(subFundGroup)) {
526                return false;
527            }
528    
529            // get the fundGroup code
530            String fundGroupCode = newAccountGlobal.getSubFundGroup().getFundGroupCode().trim();
531    
532            // if the account is part of the CG fund group, then this rule does not
533            // apply, so we're done
534            if (SpringContext.getBean(SubFundGroupService.class).isForContractsAndGrants(newAccountGlobal.getSubFundGroup())) {
535                return false;
536            }
537    
538            // at this point, we know its not a CG fund group, so we must apply the rule
539    
540            // expirationDate must be today or later than today (cannot be before today)
541            if (newExpDate.equals(today) || newExpDate.after(today)) {
542                return false;
543            }
544            else
545                return true;
546        }
547    
548    
549        /**
550         * This method tests whether the continuation account entered (if any) has expired or not.
551         * 
552         * @param accountGlobals
553         * @return true if the continuation account has expired
554         */
555        protected boolean isContinuationAccountExpired(AccountGlobal accountGlobals) {
556    
557            boolean result = false;
558    
559            String chartCode = accountGlobals.getContinuationFinChrtOfAcctCd();
560            String accountNumber = accountGlobals.getContinuationAccountNumber();
561    
562            // if either chartCode or accountNumber is not entered, then we
563            // cant continue, so exit
564            if (StringUtils.isBlank(chartCode) || StringUtils.isBlank(accountNumber)) {
565                return result;
566            }
567    
568            // attempt to retrieve the continuation account from the DB
569            Account continuation = null;
570            Map<String,String> pkMap = new HashMap<String,String>();
571            pkMap.put("chartOfAccountsCode", chartCode);
572            pkMap.put("accountNumber", accountNumber);
573            continuation = (Account) super.getBoService().findByPrimaryKey(Account.class, pkMap);
574    
575            // if the object doesnt exist, then we cant continue, so exit
576            if (ObjectUtils.isNull(continuation)) {
577                return result;
578            }
579    
580            // at this point, we have a valid continuation account, so we just need to
581            // know whether its expired or not
582            result = continuation.isExpired();
583    
584            return result;
585        }
586    
587        /**
588         * This method checks to see if any Contracts and Grants business rules were violated
589         * 
590         * @return false on rules violation
591         */
592        protected boolean checkContractsAndGrants() {
593    
594            LOG.info("checkContractsAndGrants called");
595    
596            boolean success = true;
597    
598            // Income Stream account is required if this account is CG fund group,
599            // or GF (general fund) fund group (with some exceptions)
600            success &= checkCgIncomeStreamRequired(newAccountGlobal);
601    
602            return success;
603        }
604    
605        /**
606         * This method checks to see if the contracts and grants income stream account is required
607         * 
608         * @param accountGlobals
609         * @return false if it is required (and not entered) or invalid/inactive
610         */
611        protected boolean checkCgIncomeStreamRequired(AccountGlobal accountGlobals) {
612    
613            boolean result = true;
614            boolean required = false;
615    
616            // if the subFundGroup object is null, we cant test, so exit
617            if (ObjectUtils.isNull(accountGlobals.getSubFundGroup())) {
618                return result;
619            }
620    
621            // retrieve the subfundcode and fundgroupcode
622            String subFundGroupCode = accountGlobals.getSubFundGroupCode().trim();
623            String fundGroupCode = accountGlobals.getSubFundGroup().getFundGroupCode().trim();
624    
625            // if this is a CG fund group, then its required
626            if (SpringContext.getBean(SubFundGroupService.class).isForContractsAndGrants(accountGlobals.getSubFundGroup())) {
627                required = true;
628            }
629    
630            // if this is a general fund group, then its required
631            else if (GENERAL_FUND_CD.equalsIgnoreCase(fundGroupCode)) {
632                // unless its part of the MPRACT subfundgroup
633                if (!SUB_FUND_GROUP_MEDICAL_PRACTICE_FUNDS.equalsIgnoreCase(subFundGroupCode)) {
634                    required = true;
635                }
636            }
637    
638            // if the income stream account is not required, then we're done
639            if (!required) {
640                return result;
641            }
642    
643            // make sure both coaCode and accountNumber are filled out
644            result &= checkEmptyBOField("incomeStreamAccountNumber", accountGlobals.getIncomeStreamAccountNumber(), "When Fund Group is CG or GF, Income Stream Account Number");
645            result &= checkEmptyBOField("incomeStreamFinancialCoaCode", accountGlobals.getIncomeStreamFinancialCoaCode(), "When Fund Group is CG or GF, Income Stream Chart Of Accounts Code");
646    
647            // if both fields arent present, then we're done
648            if (result == false) {
649                return result;
650            }
651    
652            // do an existence/active test
653            DictionaryValidationService dvService = super.getDictionaryValidationService();
654            boolean referenceExists = dvService.validateReferenceExists(accountGlobals, "incomeStreamAccount");
655            if (!referenceExists) {
656                putFieldError("incomeStreamAccount", KFSKeyConstants.ERROR_EXISTENCE, "Income Stream Account: " + accountGlobals.getIncomeStreamFinancialCoaCode() + "-" + accountGlobals.getIncomeStreamAccountNumber());
657                result &= false;
658            }
659    
660            return result;
661        }
662    
663        /**
664         * This method calls checkAccountDetails checkExpirationDate checkOnlyOneChartAddLineErrorWrapper whenever a new
665         * {@link AccountGlobalDetail} is added to this global
666         * 
667         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomAddCollectionLineBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument,
668         *      java.lang.String, org.kuali.rice.kns.bo.PersistableBusinessObject)
669         */
670        public boolean processCustomAddCollectionLineBusinessRules(MaintenanceDocument document, String collectionName, PersistableBusinessObject bo) {
671            AccountGlobalDetail detail = (AccountGlobalDetail) bo;
672            boolean success = true;
673    
674            success &= checkAccountDetails(detail);
675            success &= checkExpirationDate(document, detail);
676            success &= checkOnlyOneChartAddLineErrorWrapper(detail, newAccountGlobal.getAccountGlobalDetails());
677    
678            return success;
679        }
680    
681        /**
682         * This method validates that a continuation account is required and that the values provided exist
683         * 
684         * @param document An instance of the maintenance document being validated.
685         * @param newExpDate The expiration date assigned to the account being validated for submission.
686         * @return True if the continuation account values are valid for the associated account, false otherwise.
687         */
688        protected boolean checkContinuationAccount(MaintenanceDocument document, Date newExpDate) {
689            LOG.info("checkContinuationAccount called");
690    
691            boolean result = true;
692            boolean continuationAccountIsValid = true;
693    
694            // make sure both coaCode and accountNumber are filled out
695            if (ObjectUtils.isNotNull(newExpDate)) {
696                if (!checkEmptyValue(newAccountGlobal.getContinuationAccountNumber())) {
697                    putFieldError("continuationAccountNumber", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_CONTINUATION_ACCT_REQD_IF_EXP_DATE_COMPLETED);
698                    continuationAccountIsValid = false;
699                }
700                if (!checkEmptyValue(newAccountGlobal.getContinuationFinChrtOfAcctCd())) {
701                    putFieldError("continuationFinChrtOfAcctCd", KFSKeyConstants.ERROR_DOCUMENT_ACCMAINT_CONTINUATION_FINCODE_REQD_IF_EXP_DATE_COMPLETED);
702                    continuationAccountIsValid = false;
703                }
704            }
705    
706            // if both fields aren't present, then we're done
707            if (continuationAccountIsValid && ObjectUtils.isNotNull(newAccountGlobal.getContinuationAccountNumber()) && ObjectUtils.isNotNull(newAccountGlobal.getContinuationFinChrtOfAcctCd())) {
708                // do an existence/active test
709                DictionaryValidationService dvService = super.getDictionaryValidationService();
710                boolean referenceExists = dvService.validateReferenceExists(newAccountGlobal, "continuationAccount");
711                if (!referenceExists) {
712                    putFieldError("continuationAccountNumber", KFSKeyConstants.ERROR_EXISTENCE, "Continuation Account: " + newAccountGlobal.getContinuationFinChrtOfAcctCd() + "-" + newAccountGlobal.getContinuationAccountNumber());
713                    continuationAccountIsValid = false;
714                }
715            }
716    
717            if (continuationAccountIsValid) {
718                result = true;
719            }
720            else {
721                List<AccountGlobalDetail> gAcctDetails = newAccountGlobal.getAccountGlobalDetails();
722                for (AccountGlobalDetail detail : gAcctDetails) {
723                    if (null != detail.getAccountNumber() && null != newAccountGlobal.getContinuationAccountNumber()) {
724                        result &= detail.getAccountNumber().equals(newAccountGlobal.getContinuationAccountNumber());
725                        result &= detail.getChartOfAccountsCode().equals(newAccountGlobal.getContinuationFinChrtOfAcctCd());
726                    }
727                }
728            }
729    
730            return result;
731        }
732        
733        /**
734         * Validate that the object code on the form (if entered) is valid for all charts used in the detail sections.
735         * 
736         * @param acctGlobal
737         * @return
738         */
739        protected boolean checkOrganizationValidity( AccountGlobal acctGlobal ) {
740            boolean result = true;
741            
742            // check that an org has been entered
743            if ( StringUtils.isNotBlank( acctGlobal.getOrganizationCode() ) ) {           
744                // get all distinct charts
745                HashSet<String> charts = new HashSet<String>(10); 
746                for ( AccountGlobalDetail acct : acctGlobal.getAccountGlobalDetails() ) {
747                    charts.add( acct.getChartOfAccountsCode() );
748                }
749                OrganizationService orgService = SpringContext.getBean(OrganizationService.class);
750                // test for an invalid organization
751                for ( String chartCode : charts ) {
752                    if ( StringUtils.isNotBlank(chartCode) ) {
753                        if ( null == orgService.getByPrimaryIdWithCaching( chartCode, acctGlobal.getOrganizationCode() ) ) {
754                            result = false;
755                            putFieldError("organizationCode", KFSKeyConstants.ERROR_DOCUMENT_GLOBAL_ACCOUNT_INVALID_ORG, new String[] { chartCode, acctGlobal.getOrganizationCode() } );
756                            break;
757                        }
758                    }
759                }
760            }
761                    
762            return result;
763        }
764    }
765