1 /*
2 * Copyright 2005-2006 The Kuali Foundation
3 *
4 * Licensed under the Educational Community License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.opensource.org/licenses/ecl2.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.kuali.ole.coa.document.validation.impl;
17
18 import java.util.Collection;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Set;
23
24 import org.apache.commons.lang.StringUtils;
25 import org.apache.ojb.broker.PersistenceBrokerException;
26 import org.kuali.ole.coa.businessobject.BudgetAggregationCode;
27 import org.kuali.ole.coa.businessobject.IndirectCostRecoveryExclusionAccount;
28 import org.kuali.ole.coa.businessobject.ObjectCode;
29 import org.kuali.ole.coa.businessobject.ObjectConsolidation;
30 import org.kuali.ole.coa.businessobject.ObjectLevel;
31 import org.kuali.ole.coa.businessobject.OffsetDefinition;
32 import org.kuali.ole.coa.service.ChartService;
33 import org.kuali.ole.coa.service.ObjectCodeService;
34 import org.kuali.ole.coa.service.ObjectConsService;
35 import org.kuali.ole.coa.service.ObjectLevelService;
36 import org.kuali.ole.sys.OLEConstants;
37 import org.kuali.ole.sys.OLEKeyConstants;
38 import org.kuali.ole.sys.context.SpringContext;
39 import org.kuali.ole.sys.service.UniversityDateService;
40 import org.kuali.rice.core.api.config.property.ConfigurationService;
41 import org.kuali.rice.core.api.parameter.ParameterEvaluatorService;
42 import org.kuali.rice.kns.document.MaintenanceDocument;
43 import org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase;
44 import org.kuali.rice.krad.service.BusinessObjectService;
45 import org.kuali.rice.krad.util.GlobalVariables;
46
47 /**
48 * This class implements the business rules for {@link ObjectCode}
49 */
50 public class ObjectCodeRule extends MaintenanceDocumentRuleBase {
51
52 protected static ObjectLevelService objectLevelService;
53 protected static ObjectCodeService objectCodeService;
54 protected static ObjectConsService objectConsService;
55
56 protected static ConfigurationService configService;
57 protected static ChartService chartService;
58 protected Map reportsTo;
59
60 /**
61 *
62 * Constructs a ObjectCodeRule
63 * Pseudo-injects some services as well as fills out the reports to chart hierarchy
64 */
65 public ObjectCodeRule() {
66
67 if (objectConsService == null) {
68 configService = SpringContext.getBean(ConfigurationService.class);
69 objectLevelService = SpringContext.getBean(ObjectLevelService.class);
70 objectCodeService = SpringContext.getBean(ObjectCodeService.class);
71 chartService = SpringContext.getBean(ChartService.class);
72 objectConsService = SpringContext.getBean(ObjectConsService.class);
73 }
74 reportsTo = chartService.getReportsToHierarchy();
75 }
76
77
78 /**
79 * This method calls the following rules on document save:
80 * <ul>
81 * <li>{@link ObjectCodeRule#processObjectCodeRules(ObjectCode)}</li>
82 * </ul>
83 * It does not fail if rules fail
84 * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
85 */
86 @Override
87 protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) {
88
89 // default to success
90 boolean success = true;
91
92 Object maintainableObject = document.getNewMaintainableObject().getBusinessObject();
93
94 success &= processObjectCodeRules((ObjectCode) maintainableObject);
95
96 if (isObjectCodeInactivating(document)) {
97 success &= checkForBlockingOffsetDefinitions((ObjectCode)maintainableObject);
98 success &= checkForBlockingIndirectCostRecoveryExclusionAccounts((ObjectCode)maintainableObject);
99 }
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 (!/*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(ObjectCode.class, OLEConstants.ChartApcParms.OBJECT_CODE_ILLEGAL_VALUES, objCode).evaluationSucceeds()) {
149 this.putFieldError("financialObjectCode", OLEKeyConstants.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", OLEKeyConstants.ERROR_DOCUMENT_REPORTS_TO_OBJCODE_ILLEGAL, new String[] { reportsToObjectCode, calculatedReportsToChartCode });
171 result = false;
172 }
173 }
174
175 String budgetAggregationCode = objectCode.getFinancialBudgetAggregationCd();
176
177 // Start Code commented to remove Budget Aggregation Code Validation as per JIRA OLE2345
178
179 /*if (!isLegalBudgetAggregationCode(budgetAggregationCode)) {
180 this.putFieldError("financialBudgetAggregationCd", OLEKeyConstants.ERROR_DOCUMENT_OBJCODE_MUST_ONEOF_VALID, "Budget Aggregation Code");
181 result = false;
182 }
183 */
184
185 // End Code commented to remove Budget Aggregation Code Validation as per JIRA OLE2345
186
187 //KFSMI-798 - refresh() changed to refreshNonUpdateableReferences()
188 //All references for ObjectCode are non-updatable
189 objectCode.refreshNonUpdateableReferences();
190
191 // Chart code (fin_coa_cd) must be valid - handled by dd
192
193 if (!this.consolidationTableDoesNotHave(chartCode, objCode)) {
194 this.putFieldError("financialObjectCode", OLEKeyConstants.ERROR_DOCUMENT_OBJCODE_CONSOLIDATION_ERROR, chartCode + "-" + objCode);
195 result = false;
196 }
197
198 if (!this.objectLevelTableDoesNotHave(chartCode, objCode)) {
199 this.putFieldError("financialObjectCode", OLEKeyConstants.ERROR_DOCUMENT_OBJCODE_LEVEL_ERROR, chartCode + "-" + objCode);
200 result = false;
201 }
202 if (!StringUtils.isEmpty(nextYearObjectCode) && nextYearObjectCodeDoesNotExistThisYear(year, chartCode, nextYearObjectCode)) {
203 this.putFieldError("nextYearFinancialObjectCode", OLEKeyConstants.ERROR_DOCUMENT_OBJCODE_MUST_BEVALID, "Next Year Object Code");
204 result = false;
205 }
206 if (!this.isValidYear(year)) {
207 this.putFieldError("universityFiscalYear", OLEKeyConstants.ERROR_DOCUMENT_OBJCODE_MUST_BEVALID, "Fiscal Year");
208 }
209
210 result = checkResearchAdminAttributes(objectCode);
211
212 /*
213 * The framework handles this: Pending object must not have duplicates waiting for approval Description (fdoc_desc) must be
214 * entered Verify the DD handles these: Fiscal year (univ_fisal_yr) must be entered Chart code (fin_coa_code) must be
215 * entered Object code (fin_object_code) must be entered (fin_obj_cd_nm) must be entered (fin_obj_cd_shrt_nm) must be
216 * entered Object level (obj_level_code) must be entered The Reports to Object (rpts_to_fin_obj_cd) must be entered It seems
217 * like these are Business Rules for other objects: An Object code must be active when it is used as valid value in the
218 * Labor Object Code table An Object code must be active when it is used as valid value in the LD Benefits Calculation table
219 * An Object code must be active when it is used as valid value in the ICR Automated Entry table An Object code must be
220 * active when it is used as valid value in the Chart table These still need attention: Warning if chart code is inactive
221 * Warning if object level is inactive If the Next Year Object has been entered, it must exist in the object code table
222 * alongside the fiscal year and chart code (rpts_to_fin_coa_cd) is looked up based on chart code [fp_hcoat]
223 */
224
225
226 return result;
227
228 }
229
230 /**
231 * This method checks whether newly added object code already exists in Object Level table
232 *
233 * @param chartCode
234 * @param objectCode
235 * @return false if this object code already exists in the object level table
236 */
237 public boolean objectLevelTableDoesNotHave(String chartCode, String objectCode) {
238 try {
239 ObjectLevel objLevel = objectLevelService.getByPrimaryId(chartCode, objectCode);
240 if (objLevel != null) {
241 objLevel.getFinancialObjectLevelCode(); // this might throw an Exception when proxying is in effect
242 return false;
243 }
244 }
245 catch (PersistenceBrokerException e) {
246 // intentionally ignore the Exception
247 }
248
249 return true;
250 }
251
252 /**
253 *
254 * This Check whether newly added object code already exists in Consolidation table
255 * @param chartCode
256 * @param objectCode
257 * @return false if this object code already exists in the object consolidation table
258 */
259 public boolean consolidationTableDoesNotHave(String chartCode, String objectCode) {
260 try {
261 ObjectConsolidation objectCons = objectConsService.getByPrimaryId(chartCode, objectCode);
262 if (objectCons != null) {
263 objectCons.getFinConsolidationObjectCode(); // this might throw an Exception when proxying is in effect
264 return false;
265 }
266 }
267 catch (PersistenceBrokerException e) {
268 // intentionally ignore the Exception
269 }
270 return true;
271 }
272
273 /**
274 *
275 * This checks to see if the next year object code already exists in the next fiscal year
276 * @param year
277 * @param chartCode
278 * @param objCode
279 * @return false if this object code exists in the next fiscal year
280 */
281 public boolean nextYearObjectCodeDoesNotExistThisYear(Integer year, String chartCode, String objCode) {
282 try {
283 ObjectCode objectCode = objectCodeService.getByPrimaryId(year, chartCode, objCode);
284 if (objectCode != null) {
285 return false;
286 }
287 }
288 catch (PersistenceBrokerException e) {
289 // intentionally ignore the Exception
290 }
291 return true;
292 }
293
294 /**
295 *
296 * This checks to make sure the fiscal year they are trying to assign is valid
297 * @param year
298 * @return true if this is a valid year
299 */
300 /*
301 * KFSMI 5058 revised to return true value
302 *
303 */
304 @Deprecated
305 public boolean isValidYear(Integer year) {
306 return true;
307 }
308
309
310 /**
311 * This method is a null-safe wrapper around Set.contains().
312 *
313 * @param set - methods returns false if the Set is null
314 * @param value to seek
315 * @return true iff Set exists and contains given value
316 */
317 protected boolean permitted(Set set, Object value) {
318 if (set != null) {
319 return set.contains(value);
320 }
321 return false;
322 }
323
324 /**
325 *
326 * This method is a null-safe wrapper around Set.contains()
327 * @param set
328 * @param value
329 * @return true if this value is not contained in the Set or Set is null
330 */
331 protected boolean denied(List set, Object value) {
332 if (set != null) {
333 return !set.contains(value);
334 }
335 return true;
336 }
337
338 /**
339 * Budget Aggregation Code (fobj_bdgt_aggr_cd) must have an institutionally specified value
340 *
341 * @param budgetAggregationCode
342 * @return true if this is a legal budget aggregation code
343 */
344 protected boolean isLegalBudgetAggregationCode(String budgetAggregationCode) {
345
346 // find all the matching records
347 Map whereMap = new HashMap();
348 whereMap.put("code", budgetAggregationCode);
349
350 Collection budgetAggregationCodes = getBoService().findMatching(BudgetAggregationCode.class, whereMap);
351
352 // if there is at least one result, then entered budget aggregation code is legal
353 return budgetAggregationCodes.size() > 0;
354 }
355
356 /**
357 *
358 * This checks to see if the object code already exists in the system
359 * @param year
360 * @param chart
361 * @param objectCode
362 * @return true if it exists
363 */
364 protected boolean verifyObjectCode(Integer year, String chart, String objectCode) {
365 return null != objectCodeService.getByPrimaryId(year, chart, objectCode);
366 }
367
368 /**
369 *
370 * This method checks When the value of reportsToChartCode does not have an institutional exception, the Reports to Object
371 * (rpts_to_fin_obj_cd) fiscal year, and chart code must exist in the object code table
372 * if the chart and object are the same, then skip the check
373 * this assumes that the validity of the reports-to object code has already been tested (and corrected if necessary)
374 * @param year
375 * @param chart
376 * @param objectCode
377 * @param reportsToChartCode
378 * @param reportsToObjectCode
379 * @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
380 * or if the object code already exists
381 */
382 protected boolean verifyReportsToChartCode(Integer year, String chart, String objectCode, String reportsToChartCode, String reportsToObjectCode) {
383 // TODO: verify this ambiguously stated rule against the UNIFACE source
384 // When the value of reportsToChartCode does not have an institutional exception, the Reports to Object
385 // (rpts_to_fin_obj_cd) fiscal year, and chart code must exist in the object code table
386
387 // if the chart and object are the same, then skip the check
388 // this assumes that the validity of the reports-to object code has already been tested (and corrected if necessary)
389 if (StringUtils.equals(reportsToChartCode, chart) && StringUtils.equals(reportsToObjectCode, objectCode)) {
390 return true;
391 }
392
393 // otherwise, check if the object is valid
394 return verifyObjectCode(year, reportsToChartCode, reportsToObjectCode);
395 }
396
397 /**
398 * Determines if the given maintenance document constitutes an inactivation of the object code it is maintaining
399 * @param maintenanceDocument the maintenance document maintaining an object code
400 * @return true if the document is inactivating the object code, false otherwise
401 */
402 protected boolean isObjectCodeInactivating(MaintenanceDocument maintenanceDocument) {
403 if (maintenanceDocument.isEdit() && maintenanceDocument.getOldMaintainableObject() != null && maintenanceDocument.getOldMaintainableObject().getBusinessObject() != null) {
404 final ObjectCode oldObjectCode = (ObjectCode)maintenanceDocument.getOldMaintainableObject().getBusinessObject();
405 final ObjectCode newObjectCode = (ObjectCode)maintenanceDocument.getNewMaintainableObject().getBusinessObject();
406
407 return oldObjectCode.isActive() && !newObjectCode.isActive();
408 }
409 return false;
410 }
411
412 /**
413 * Checks that no offset definitions are dependent on the given object code if it is inactivated
414 * @param objectCode the object code trying to inactivate
415 * @return true if no offset definitions rely on the object code, false otherwise; this method also inserts error statements
416 */
417 protected boolean checkForBlockingOffsetDefinitions(ObjectCode objectCode) {
418 final BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class);
419 boolean result = true;
420
421 Map<String, Object> keys = new HashMap<String, Object>();
422 keys.put("universityFiscalYear", objectCode.getUniversityFiscalYear());
423 keys.put("chartOfAccountsCode", objectCode.getChartOfAccountsCode());
424 keys.put("financialObjectCode", objectCode.getFinancialObjectCode());
425
426 final int matchingCount = businessObjectService.countMatching(OffsetDefinition.class, keys);
427 if (matchingCount > 0) {
428 GlobalVariables.getMessageMap().putErrorForSectionId("Edit Object Code",OLEKeyConstants.ERROR_DOCUMENT_OBJECTMAINT_INACTIVATION_BLOCKING,new String[] {(objectCode.getUniversityFiscalYear() != null ? objectCode.getUniversityFiscalYear().toString() : ""), objectCode.getChartOfAccountsCode(), objectCode.getFinancialObjectCode(), Integer.toString(matchingCount), OffsetDefinition.class.getName()});
429 result = false;
430 }
431 return result;
432 }
433
434 /**
435 * Checks that no ICR Exclusion by Account records are dependent on the given object code if it is inactivated
436 * @param objectCode the object code trying to inactivate
437 * @return if no ICR Exclusion by Account records rely on the object code, false otherwise; this method also inserts error statements
438 */
439 protected boolean checkForBlockingIndirectCostRecoveryExclusionAccounts(ObjectCode objectCode) {
440 boolean result = true;
441
442 final UniversityDateService universityDateService = SpringContext.getBean(UniversityDateService.class);
443 if (objectCode.getUniversityFiscalYear() != null && objectCode.getUniversityFiscalYear().equals(universityDateService.getCurrentFiscalYear())) {
444 final BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class);
445
446 Map<String, Object> keys = new HashMap<String, Object>();
447 keys.put("chartOfAccountsCode", objectCode.getChartOfAccountsCode());
448 keys.put("financialObjectCode", objectCode.getFinancialObjectCode());
449
450 final int matchingCount = businessObjectService.countMatching(IndirectCostRecoveryExclusionAccount.class, keys);
451 if (matchingCount > 0) {
452 GlobalVariables.getMessageMap().putErrorForSectionId("Edit Object Code",OLEKeyConstants.ERROR_DOCUMENT_OBJECTMAINT_INACTIVATION_BLOCKING,new String[] {(objectCode.getUniversityFiscalYear() != null ? objectCode.getUniversityFiscalYear().toString() : ""), objectCode.getChartOfAccountsCode(), objectCode.getFinancialObjectCode(), Integer.toString(matchingCount), IndirectCostRecoveryExclusionAccount.class.getName()});
453 result = false;
454 }
455 }
456 return result;
457 }
458
459 /**
460 *
461 * This method verifies the budget category value
462 * @param objectCodeGlobal
463 * @return true if valid
464 */
465 protected boolean checkResearchAdminAttributes(ObjectCode objectCode) {
466
467 // String budgetCategoryCode = objectCode.getRschBudgetCategoryCode();
468
469 // if (StringUtils.isNotEmpty(budgetCategoryCode)) {
470 // List<BudgetCategoryDTO> budgetCategoryList = new ArrayList<BudgetCategoryDTO>();
471 // HashMap<String, String> criteria = new HashMap<String, String>();
472 // criteria.put("budgetCategoryCode", budgetCategoryCode);
473 // BudgetCategoryService budgetCategoryService = (BudgetCategoryService) GlobalResourceLoader.getService(new QName(OLEConstants.Reserch.KC_NAMESPACE_URI, OLEConstants.Reserch.KC_BUDGET_CATEGORY_SERVICE));
474 // budgetCategoryList = budgetCategoryService.lookupBudgetCategories(criteria);
475 // if (budgetCategoryList == null || budgetCategoryList.isEmpty()) {
476 // GlobalVariables.getMessageMap().putErrorForSectionId(KcConstants.BudgetAdjustmentService.SECTION_ID_RESEARCH_ADMIN_ATTRIBUTES, OLEKeyConstants.ERROR_DOCUMENT_OBJECTMAINT_BUDGET_CATEGORY_CODE, "Budget Category Code");
477 // return false;
478 // }
479 // }
480
481 return true;
482 }
483 }