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    }