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