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.util.Collection; 019 import java.util.HashMap; 020 import java.util.List; 021 import java.util.Map; 022 import java.util.Set; 023 024 import org.apache.commons.lang.StringUtils; 025 import org.apache.ojb.broker.PersistenceBrokerException; 026 import org.kuali.kfs.coa.businessobject.BudgetAggregationCode; 027 import org.kuali.kfs.coa.businessobject.IndirectCostRecoveryExclusionAccount; 028 import org.kuali.kfs.coa.businessobject.ObjectCode; 029 import org.kuali.kfs.coa.businessobject.ObjectConsolidation; 030 import org.kuali.kfs.coa.businessobject.ObjectLevel; 031 import org.kuali.kfs.coa.businessobject.OffsetDefinition; 032 import org.kuali.kfs.coa.service.ChartService; 033 import org.kuali.kfs.coa.service.ObjectCodeService; 034 import org.kuali.kfs.coa.service.ObjectConsService; 035 import org.kuali.kfs.coa.service.ObjectLevelService; 036 import org.kuali.kfs.sys.KFSConstants; 037 import org.kuali.kfs.sys.KFSKeyConstants; 038 import org.kuali.kfs.sys.context.SpringContext; 039 import org.kuali.kfs.sys.service.UniversityDateService; 040 import org.kuali.rice.kns.document.MaintenanceDocument; 041 import org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase; 042 import org.kuali.rice.kns.service.BusinessObjectService; 043 import org.kuali.rice.kns.service.KualiConfigurationService; 044 import org.kuali.rice.kns.service.ParameterService; 045 import org.kuali.rice.kns.util.GlobalVariables; 046 047 /** 048 * This class implements the business rules for {@link ObjectCode} 049 */ 050 public class ObjectCodeRule extends MaintenanceDocumentRuleBase { 051 052 protected static ObjectLevelService objectLevelService; 053 protected static ObjectCodeService objectCodeService; 054 protected static ObjectConsService objectConsService; 055 056 protected static KualiConfigurationService configService; 057 protected static ChartService chartService; 058 protected Map reportsTo; 059 060 /** 061 * 062 * Constructs a ObjectCodeRule 063 * Pseudo-injects some services as well as fills out the reports to chart hierarchy 064 */ 065 public ObjectCodeRule() { 066 067 if (objectConsService == null) { 068 configService = SpringContext.getBean(KualiConfigurationService.class); 069 objectLevelService = SpringContext.getBean(ObjectLevelService.class); 070 objectCodeService = SpringContext.getBean(ObjectCodeService.class); 071 chartService = SpringContext.getBean(ChartService.class); 072 objectConsService = SpringContext.getBean(ObjectConsService.class); 073 } 074 reportsTo = chartService.getReportsToHierarchy(); 075 } 076 077 078 /** 079 * This method calls the following rules on document save: 080 * <ul> 081 * <li>{@link ObjectCodeRule#processObjectCodeRules(ObjectCode)}</li> 082 * </ul> 083 * It does not fail if rules fail 084 * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument) 085 */ 086 @Override 087 protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) { 088 089 // default to success 090 boolean success = true; 091 092 Object maintainableObject = document.getNewMaintainableObject().getBusinessObject(); 093 094 success &= processObjectCodeRules((ObjectCode) maintainableObject); 095 096 if (isObjectCodeInactivating(document)) { 097 success &= checkForBlockingOffsetDefinitions((ObjectCode)maintainableObject); 098 success &= checkForBlockingIndirectCostRecoveryExclusionAccounts((ObjectCode)maintainableObject); 099 } 100 101 return success; 102 103 } 104 105 /** 106 * This method calls the following rules on document route: 107 * <ul> 108 * <li>{@link ObjectCodeRule#processObjectCodeRules(ObjectCode)}</li> 109 * </ul> 110 * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomRouteDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument) 111 */ 112 @Override 113 protected boolean processCustomRouteDocumentBusinessRules(MaintenanceDocument document) { 114 LOG.debug("processCustomRouteDocumentBusinessRules called"); 115 116 boolean success = true; 117 118 Object maintainableObject = document.getNewMaintainableObject().getBusinessObject(); 119 success &= processObjectCodeRules((ObjectCode) maintainableObject); 120 121 if (isObjectCodeInactivating(document)) { 122 success &= checkForBlockingOffsetDefinitions((ObjectCode)maintainableObject); 123 success &= checkForBlockingIndirectCostRecoveryExclusionAccounts((ObjectCode)maintainableObject); 124 } 125 126 return success; 127 } 128 129 /** 130 * 131 * This checks the following rules: 132 * <ul> 133 * <li>object code valid</li> 134 * <li>reports to chart code is valid (similar to what {@link ObjectCodePreRules} does)</li> 135 * <li>is the budget aggregation code valid</li> 136 * <li>then checks to make sure that this object code hasn't already been entered in the consolidation and level table</li> 137 * <li>finally checks to make sure that the next year object code (if filled out) isn't already in there and that this object code has a valid fiscal year</li> 138 * </ul> 139 * @param objectCode 140 * @return 141 */ 142 protected boolean processObjectCodeRules(ObjectCode objectCode) { 143 144 boolean result = true; 145 146 String objCode = objectCode.getFinancialObjectCode(); 147 148 if (!SpringContext.getBean(ParameterService.class).getParameterEvaluator(ObjectCode.class, KFSConstants.ChartApcParms.OBJECT_CODE_ILLEGAL_VALUES, objCode).evaluationSucceeds()) { 149 this.putFieldError("financialObjectCode", KFSKeyConstants.ERROR_DOCUMENT_OBJCODE_ILLEGAL, objCode); 150 result = false; 151 } 152 153 Integer year = objectCode.getUniversityFiscalYear(); 154 String chartCode = objectCode.getChartOfAccountsCode(); 155 String calculatedReportsToChartCode; 156 String reportsToObjectCode = objectCode.getReportsToFinancialObjectCode(); 157 String nextYearObjectCode = objectCode.getNextYearFinancialObjectCode(); 158 159 // only validate if chartCode is NOT null ( chartCode should be provided to get determine reportsToChartCode ) 160 if (chartCode != null) { 161 162 // We must calculate a reportsToChartCode here to duplicate the logic 163 // that takes place in the preRule. 164 // The reason is that when we do a SAVE, the pre-rules are not 165 // run and we will get bogus error messages. 166 // So, we are simulating here what the pre-rule will do. 167 calculatedReportsToChartCode = (String) reportsTo.get(chartCode); 168 169 if (!verifyReportsToChartCode(year, chartCode, objectCode.getFinancialObjectCode(), calculatedReportsToChartCode, reportsToObjectCode)) { 170 this.putFieldError("reportsToFinancialObjectCode", KFSKeyConstants.ERROR_DOCUMENT_REPORTS_TO_OBJCODE_ILLEGAL, new String[] { reportsToObjectCode, calculatedReportsToChartCode }); 171 result = false; 172 } 173 } 174 175 String budgetAggregationCode = objectCode.getFinancialBudgetAggregationCd(); 176 177 if (!isLegalBudgetAggregationCode(budgetAggregationCode)) { 178 this.putFieldError("financialBudgetAggregationCd", KFSKeyConstants.ERROR_DOCUMENT_OBJCODE_MUST_ONEOF_VALID, "Budget Aggregation Code"); 179 result = false; 180 } 181 182 //KFSMI-798 - refresh() changed to refreshNonUpdateableReferences() 183 //All references for ObjectCode are non-updatable 184 objectCode.refreshNonUpdateableReferences(); 185 186 // Chart code (fin_coa_cd) must be valid - handled by dd 187 188 if (!this.consolidationTableDoesNotHave(chartCode, objCode)) { 189 this.putFieldError("financialObjectCode", KFSKeyConstants.ERROR_DOCUMENT_OBJCODE_CONSOLIDATION_ERROR, chartCode + "-" + objCode); 190 result = false; 191 } 192 193 if (!this.objectLevelTableDoesNotHave(chartCode, objCode)) { 194 this.putFieldError("financialObjectCode", KFSKeyConstants.ERROR_DOCUMENT_OBJCODE_LEVEL_ERROR, chartCode + "-" + objCode); 195 result = false; 196 } 197 if (!StringUtils.isEmpty(nextYearObjectCode) && nextYearObjectCodeDoesNotExistThisYear(year, chartCode, nextYearObjectCode)) { 198 this.putFieldError("nextYearFinancialObjectCode", KFSKeyConstants.ERROR_DOCUMENT_OBJCODE_MUST_BEVALID, "Next Year Object Code"); 199 result = false; 200 } 201 if (!this.isValidYear(year)) { 202 this.putFieldError("universityFiscalYear", KFSKeyConstants.ERROR_DOCUMENT_OBJCODE_MUST_BEVALID, "Fiscal Year"); 203 } 204 205 /* 206 * The framework handles this: Pending object must not have duplicates waiting for approval Description (fdoc_desc) must be 207 * entered Verify the DD handles these: Fiscal year (univ_fisal_yr) must be entered Chart code (fin_coa_code) must be 208 * entered Object code (fin_object_code) must be entered (fin_obj_cd_nm) must be entered (fin_obj_cd_shrt_nm) must be 209 * entered Object level (obj_level_code) must be entered The Reports to Object (rpts_to_fin_obj_cd) must be entered It seems 210 * like these are Business Rules for other objects: An Object code must be active when it is used as valid value in the 211 * Labor Object Code table An Object code must be active when it is used as valid value in the LD Benefits Calculation table 212 * An Object code must be active when it is used as valid value in the ICR Automated Entry table An Object code must be 213 * active when it is used as valid value in the Chart table These still need attention: Warning if chart code is inactive 214 * Warning if object level is inactive If the Next Year Object has been entered, it must exist in the object code table 215 * alongside the fiscal year and chart code (rpts_to_fin_coa_cd) is looked up based on chart code [fp_hcoat] 216 */ 217 218 219 return result; 220 221 } 222 223 /** 224 * This method checks whether newly added object code already exists in Object Level table 225 * 226 * @param chartCode 227 * @param objectCode 228 * @return false if this object code already exists in the object level table 229 */ 230 public boolean objectLevelTableDoesNotHave(String chartCode, String objectCode) { 231 try { 232 ObjectLevel objLevel = objectLevelService.getByPrimaryId(chartCode, objectCode); 233 if (objLevel != null) { 234 objLevel.getFinancialObjectLevelCode(); // this might throw an Exception when proxying is in effect 235 return false; 236 } 237 } 238 catch (PersistenceBrokerException e) { 239 // intentionally ignore the Exception 240 } 241 242 return true; 243 } 244 245 /** 246 * 247 * This Check whether newly added object code already exists in Consolidation table 248 * @param chartCode 249 * @param objectCode 250 * @return false if this object code already exists in the object consolidation table 251 */ 252 public boolean consolidationTableDoesNotHave(String chartCode, String objectCode) { 253 try { 254 ObjectConsolidation objectCons = objectConsService.getByPrimaryId(chartCode, objectCode); 255 if (objectCons != null) { 256 objectCons.getFinConsolidationObjectCode(); // this might throw an Exception when proxying is in effect 257 return false; 258 } 259 } 260 catch (PersistenceBrokerException e) { 261 // intentionally ignore the Exception 262 } 263 return true; 264 } 265 266 /** 267 * 268 * This checks to see if the next year object code already exists in the next fiscal year 269 * @param year 270 * @param chartCode 271 * @param objCode 272 * @return false if this object code exists in the next fiscal year 273 */ 274 public boolean nextYearObjectCodeDoesNotExistThisYear(Integer year, String chartCode, String objCode) { 275 try { 276 ObjectCode objectCode = objectCodeService.getByPrimaryId(year, chartCode, objCode); 277 if (objectCode != null) { 278 return false; 279 } 280 } 281 catch (PersistenceBrokerException e) { 282 // intentionally ignore the Exception 283 } 284 return true; 285 } 286 287 /** 288 * 289 * This checks to make sure the fiscal year they are trying to assign is valid 290 * @param year 291 * @return true if this is a valid year 292 */ 293 /* 294 * KFSMI 5058 revised to return true value 295 * 296 */ 297 @Deprecated 298 public boolean isValidYear(Integer year) { 299 return true; 300 } 301 302 303 /** 304 * This method is a null-safe wrapper around Set.contains(). 305 * 306 * @param set - methods returns false if the Set is null 307 * @param value to seek 308 * @return true iff Set exists and contains given value 309 */ 310 protected boolean permitted(Set set, Object value) { 311 if (set != null) { 312 return set.contains(value); 313 } 314 return false; 315 } 316 317 /** 318 * 319 * This method is a null-safe wrapper around Set.contains() 320 * @param set 321 * @param value 322 * @return true if this value is not contained in the Set or Set is null 323 */ 324 protected boolean denied(List set, Object value) { 325 if (set != null) { 326 return !set.contains(value); 327 } 328 return true; 329 } 330 331 /** 332 * Budget Aggregation Code (fobj_bdgt_aggr_cd) must have an institutionally specified value 333 * 334 * @param budgetAggregationCode 335 * @return true if this is a legal budget aggregation code 336 */ 337 protected boolean isLegalBudgetAggregationCode(String budgetAggregationCode) { 338 339 // find all the matching records 340 Map whereMap = new HashMap(); 341 whereMap.put("code", budgetAggregationCode); 342 343 Collection budgetAggregationCodes = getBoService().findMatching(BudgetAggregationCode.class, whereMap); 344 345 // if there is at least one result, then entered budget aggregation code is legal 346 return budgetAggregationCodes.size() > 0; 347 } 348 349 /** 350 * 351 * This checks to see if the object code already exists in the system 352 * @param year 353 * @param chart 354 * @param objectCode 355 * @return true if it exists 356 */ 357 protected boolean verifyObjectCode(Integer year, String chart, String objectCode) { 358 return null != objectCodeService.getByPrimaryId(year, chart, objectCode); 359 } 360 361 /** 362 * 363 * This method checks When the value of reportsToChartCode does not have an institutional exception, the Reports to Object 364 * (rpts_to_fin_obj_cd) fiscal year, and chart code must exist in the object code table 365 * if the chart and object are the same, then skip the check 366 * this assumes that the validity of the reports-to object code has already been tested (and corrected if necessary) 367 * @param year 368 * @param chart 369 * @param objectCode 370 * @param reportsToChartCode 371 * @param reportsToObjectCode 372 * @return true if the object code's reports to chart and chart are the same and reports to object and object code are the same 373 * or if the object code already exists 374 */ 375 protected boolean verifyReportsToChartCode(Integer year, String chart, String objectCode, String reportsToChartCode, String reportsToObjectCode) { 376 // TODO: verify this ambiguously stated rule against the UNIFACE source 377 // When the value of reportsToChartCode does not have an institutional exception, the Reports to Object 378 // (rpts_to_fin_obj_cd) fiscal year, and chart code must exist in the object code table 379 380 // if the chart and object are the same, then skip the check 381 // this assumes that the validity of the reports-to object code has already been tested (and corrected if necessary) 382 if (StringUtils.equals(reportsToChartCode, chart) && StringUtils.equals(reportsToObjectCode, objectCode)) { 383 return true; 384 } 385 386 // otherwise, check if the object is valid 387 return verifyObjectCode(year, reportsToChartCode, reportsToObjectCode); 388 } 389 390 /** 391 * Determines if the given maintenance document constitutes an inactivation of the object code it is maintaining 392 * @param maintenanceDocument the maintenance document maintaining an object code 393 * @return true if the document is inactivating the object code, false otherwise 394 */ 395 protected boolean isObjectCodeInactivating(MaintenanceDocument maintenanceDocument) { 396 if (maintenanceDocument.isEdit() && maintenanceDocument.getOldMaintainableObject() != null && maintenanceDocument.getOldMaintainableObject().getBusinessObject() != null) { 397 final ObjectCode oldObjectCode = (ObjectCode)maintenanceDocument.getOldMaintainableObject().getBusinessObject(); 398 final ObjectCode newObjectCode = (ObjectCode)maintenanceDocument.getNewMaintainableObject().getBusinessObject(); 399 400 return oldObjectCode.isActive() && !newObjectCode.isActive(); 401 } 402 return false; 403 } 404 405 /** 406 * Checks that no offset definitions are dependent on the given object code if it is inactivated 407 * @param objectCode the object code trying to inactivate 408 * @return true if no offset definitions rely on the object code, false otherwise; this method also inserts error statements 409 */ 410 protected boolean checkForBlockingOffsetDefinitions(ObjectCode objectCode) { 411 final BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class); 412 boolean result = true; 413 414 Map<String, Object> keys = new HashMap<String, Object>(); 415 keys.put("universityFiscalYear", objectCode.getUniversityFiscalYear()); 416 keys.put("chartOfAccountsCode", objectCode.getChartOfAccountsCode()); 417 keys.put("financialObjectCode", objectCode.getFinancialObjectCode()); 418 419 final int matchingCount = businessObjectService.countMatching(OffsetDefinition.class, keys); 420 if (matchingCount > 0) { 421 GlobalVariables.getMessageMap().putErrorForSectionId("Edit Object Code",KFSKeyConstants.ERROR_DOCUMENT_OBJECTMAINT_INACTIVATION_BLOCKING,new String[] {(objectCode.getUniversityFiscalYear() != null ? objectCode.getUniversityFiscalYear().toString() : ""), objectCode.getChartOfAccountsCode(), objectCode.getFinancialObjectCode(), Integer.toString(matchingCount), OffsetDefinition.class.getName()}); 422 result = false; 423 } 424 return result; 425 } 426 427 /** 428 * Checks that no ICR Exclusion by Account records are dependent on the given object code if it is inactivated 429 * @param objectCode the object code trying to inactivate 430 * @return if no ICR Exclusion by Account records rely on the object code, false otherwise; this method also inserts error statements 431 */ 432 protected boolean checkForBlockingIndirectCostRecoveryExclusionAccounts(ObjectCode objectCode) { 433 boolean result = true; 434 435 final UniversityDateService universityDateService = SpringContext.getBean(UniversityDateService.class); 436 if (objectCode.getUniversityFiscalYear() != null && objectCode.getUniversityFiscalYear().equals(universityDateService.getCurrentFiscalYear())) { 437 final BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class); 438 439 Map<String, Object> keys = new HashMap<String, Object>(); 440 keys.put("chartOfAccountsCode", objectCode.getChartOfAccountsCode()); 441 keys.put("financialObjectCode", objectCode.getFinancialObjectCode()); 442 443 final int matchingCount = businessObjectService.countMatching(IndirectCostRecoveryExclusionAccount.class, keys); 444 if (matchingCount > 0) { 445 GlobalVariables.getMessageMap().putErrorForSectionId("Edit Object Code",KFSKeyConstants.ERROR_DOCUMENT_OBJECTMAINT_INACTIVATION_BLOCKING,new String[] {(objectCode.getUniversityFiscalYear() != null ? objectCode.getUniversityFiscalYear().toString() : ""), objectCode.getChartOfAccountsCode(), objectCode.getFinancialObjectCode(), Integer.toString(matchingCount), IndirectCostRecoveryExclusionAccount.class.getName()}); 446 result = false; 447 } 448 } 449 return result; 450 } 451 452 }