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