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 }