View Javadoc
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 }