/*
* The Kuali Financial System, a comprehensive financial management system for higher education.
*
* Copyright 2005-2014 The Kuali Foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kuali.kfs.coa.document.validation.impl;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.ojb.broker.PersistenceBrokerException;
import org.kuali.kfs.coa.businessobject.BudgetAggregationCode;
import org.kuali.kfs.coa.businessobject.IndirectCostRecoveryExclusionAccount;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coa.businessobject.ObjectConsolidation;
import org.kuali.kfs.coa.businessobject.ObjectLevel;
import org.kuali.kfs.coa.businessobject.OffsetDefinition;
import org.kuali.kfs.coa.service.ChartService;
import org.kuali.kfs.coa.service.ObjectCodeService;
import org.kuali.kfs.coa.service.ObjectConsService;
import org.kuali.kfs.coa.service.ObjectLevelService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.parameter.ParameterEvaluatorService;
import org.kuali.rice.kns.document.MaintenanceDocument;
import org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase;
import org.kuali.rice.krad.service.BusinessObjectService;
import org.kuali.rice.krad.util.GlobalVariables;
/**
* This class implements the business rules for {@link ObjectCode}
*/
public class ObjectCodeRule extends MaintenanceDocumentRuleBase {
protected static ObjectLevelService objectLevelService;
protected static ObjectCodeService objectCodeService;
protected static ObjectConsService objectConsService;
protected static ConfigurationService configService;
protected static ChartService chartService;
protected Map reportsTo;
/**
*
* Constructs a ObjectCodeRule
* Pseudo-injects some services as well as fills out the reports to chart hierarchy
*/
public ObjectCodeRule() {
if (objectConsService == null) {
configService = SpringContext.getBean(ConfigurationService.class);
objectLevelService = SpringContext.getBean(ObjectLevelService.class);
objectCodeService = SpringContext.getBean(ObjectCodeService.class);
chartService = SpringContext.getBean(ChartService.class);
objectConsService = SpringContext.getBean(ObjectConsService.class);
}
reportsTo = chartService.getReportsToHierarchy();
}
/**
* This method calls the following rules on document save:
* <ul>
* <li>{@link ObjectCodeRule#processObjectCodeRules(ObjectCode)}</li>
* </ul>
* It does not fail if rules fail
* @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
*/
@Override
protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) {
// default to success
boolean success = true;
Object maintainableObject = document.getNewMaintainableObject().getBusinessObject();
success &= processObjectCodeRules((ObjectCode) maintainableObject);
if (isObjectCodeInactivating(document)) {
success &= checkForBlockingOffsetDefinitions((ObjectCode)maintainableObject);
success &= checkForBlockingIndirectCostRecoveryExclusionAccounts((ObjectCode)maintainableObject);
}
return success;
}
/**
* This method calls the following rules on document route:
* <ul>
* <li>{@link ObjectCodeRule#processObjectCodeRules(ObjectCode)}</li>
* </ul>
* @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomRouteDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
*/
@Override
protected boolean processCustomRouteDocumentBusinessRules(MaintenanceDocument document) {
LOG.debug("processCustomRouteDocumentBusinessRules called");
boolean success = true;
Object maintainableObject = document.getNewMaintainableObject().getBusinessObject();
success &= processObjectCodeRules((ObjectCode) maintainableObject);
if (isObjectCodeInactivating(document)) {
success &= checkForBlockingOffsetDefinitions((ObjectCode)maintainableObject);
success &= checkForBlockingIndirectCostRecoveryExclusionAccounts((ObjectCode)maintainableObject);
}
return success;
}
/**
*
* This checks the following rules:
* <ul>
* <li>object code valid</li>
* <li>reports to chart code is valid (similar to what {@link ObjectCodePreRules} does)</li>
* <li>is the budget aggregation code valid</li>
* <li>then checks to make sure that this object code hasn't already been entered in the consolidation and level table</li>
* <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>
* </ul>
* @param objectCode
* @return
*/
protected boolean processObjectCodeRules(ObjectCode objectCode) {
boolean result = true;
String objCode = objectCode.getFinancialObjectCode();
if (!/*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(ObjectCode.class, KFSConstants.ChartApcParms.OBJECT_CODE_ILLEGAL_VALUES, objCode).evaluationSucceeds()) {
this.putFieldError("financialObjectCode", KFSKeyConstants.ERROR_DOCUMENT_OBJCODE_ILLEGAL, objCode);
result = false;
}
Integer year = objectCode.getUniversityFiscalYear();
String chartCode = objectCode.getChartOfAccountsCode();
String calculatedReportsToChartCode;
String reportsToObjectCode = objectCode.getReportsToFinancialObjectCode();
String nextYearObjectCode = objectCode.getNextYearFinancialObjectCode();
// only validate if chartCode is NOT null ( chartCode should be provided to get determine reportsToChartCode )
if (chartCode != null) {
// We must calculate a reportsToChartCode here to duplicate the logic
// that takes place in the preRule.
// The reason is that when we do a SAVE, the pre-rules are not
// run and we will get bogus error messages.
// So, we are simulating here what the pre-rule will do.
calculatedReportsToChartCode = (String) reportsTo.get(chartCode);
if (!verifyReportsToChartCode(year, chartCode, objectCode.getFinancialObjectCode(), calculatedReportsToChartCode, reportsToObjectCode)) {
this.putFieldError("reportsToFinancialObjectCode", KFSKeyConstants.ERROR_DOCUMENT_REPORTS_TO_OBJCODE_ILLEGAL, new String[] { reportsToObjectCode, calculatedReportsToChartCode });
result = false;
}
}
String budgetAggregationCode = objectCode.getFinancialBudgetAggregationCd();
if (!isLegalBudgetAggregationCode(budgetAggregationCode)) {
this.putFieldError("financialBudgetAggregationCd", KFSKeyConstants.ERROR_DOCUMENT_OBJCODE_MUST_ONEOF_VALID, "Budget Aggregation Code");
result = false;
}
//KFSMI-798 - refresh() changed to refreshNonUpdateableReferences()
//All references for ObjectCode are non-updatable
objectCode.refreshNonUpdateableReferences();
// Chart code (fin_coa_cd) must be valid - handled by dd
if (!this.consolidationTableDoesNotHave(chartCode, objCode)) {
this.putFieldError("financialObjectCode", KFSKeyConstants.ERROR_DOCUMENT_OBJCODE_CONSOLIDATION_ERROR, chartCode + "-" + objCode);
result = false;
}
if (!this.objectLevelTableDoesNotHave(chartCode, objCode)) {
this.putFieldError("financialObjectCode", KFSKeyConstants.ERROR_DOCUMENT_OBJCODE_LEVEL_ERROR, chartCode + "-" + objCode);
result = false;
}
if (!StringUtils.isEmpty(nextYearObjectCode) && nextYearObjectCodeDoesNotExistThisYear(year, chartCode, nextYearObjectCode)) {
this.putFieldError("nextYearFinancialObjectCode", KFSKeyConstants.ERROR_DOCUMENT_OBJCODE_MUST_BEVALID, "Next Year Object Code");
result = false;
}
if (!this.isValidYear(year)) {
this.putFieldError("universityFiscalYear", KFSKeyConstants.ERROR_DOCUMENT_OBJCODE_MUST_BEVALID, "Fiscal Year");
}
/*
* The framework handles this: Pending object must not have duplicates waiting for approval Description (fdoc_desc) must be
* entered Verify the DD handles these: Fiscal year (univ_fisal_yr) must be entered Chart code (fin_coa_code) must be
* entered Object code (fin_object_code) must be entered (fin_obj_cd_nm) must be entered (fin_obj_cd_shrt_nm) must be
* entered Object level (obj_level_code) must be entered The Reports to Object (rpts_to_fin_obj_cd) must be entered It seems
* like these are Business Rules for other objects: An Object code must be active when it is used as valid value in the
* Labor Object Code table An Object code must be active when it is used as valid value in the LD Benefits Calculation table
* An Object code must be active when it is used as valid value in the ICR Automated Entry table An Object code must be
* active when it is used as valid value in the Chart table These still need attention: Warning if chart code is inactive
* Warning if object level is inactive If the Next Year Object has been entered, it must exist in the object code table
* alongside the fiscal year and chart code (rpts_to_fin_coa_cd) is looked up based on chart code [fp_hcoat]
*/
return result;
}
/**
* This method checks whether newly added object code already exists in Object Level table
*
* @param chartCode
* @param objectCode
* @return false if this object code already exists in the object level table
*/
public boolean objectLevelTableDoesNotHave(String chartCode, String objectCode) {
try {
ObjectLevel objLevel = objectLevelService.getByPrimaryId(chartCode, objectCode);
if (objLevel != null) {
objLevel.getFinancialObjectLevelCode(); // this might throw an Exception when proxying is in effect
return false;
}
}
catch (PersistenceBrokerException e) {
// intentionally ignore the Exception
}
return true;
}
/**
*
* This Check whether newly added object code already exists in Consolidation table
* @param chartCode
* @param objectCode
* @return false if this object code already exists in the object consolidation table
*/
public boolean consolidationTableDoesNotHave(String chartCode, String objectCode) {
try {
ObjectConsolidation objectCons = objectConsService.getByPrimaryId(chartCode, objectCode);
if (objectCons != null) {
objectCons.getFinConsolidationObjectCode(); // this might throw an Exception when proxying is in effect
return false;
}
}
catch (PersistenceBrokerException e) {
// intentionally ignore the Exception
}
return true;
}
/**
*
* This checks to see if the next year object code already exists in the next fiscal year
* @param year
* @param chartCode
* @param objCode
* @return false if this object code exists in the next fiscal year
*/
public boolean nextYearObjectCodeDoesNotExistThisYear(Integer year, String chartCode, String objCode) {
try {
ObjectCode objectCode = objectCodeService.getByPrimaryId(year, chartCode, objCode);
if (objectCode != null) {
return false;
}
}
catch (PersistenceBrokerException e) {
// intentionally ignore the Exception
}
return true;
}
/**
*
* This checks to make sure the fiscal year they are trying to assign is valid
* @param year
* @return true if this is a valid year
*/
/*
* KFSMI 5058 revised to return true value
*
*/
@Deprecated
public boolean isValidYear(Integer year) {
return true;
}
/**
* This method is a null-safe wrapper around Set.contains().
*
* @param set - methods returns false if the Set is null
* @param value to seek
* @return true iff Set exists and contains given value
*/
protected boolean permitted(Set set, Object value) {
if (set != null) {
return set.contains(value);
}
return false;
}
/**
*
* This method is a null-safe wrapper around Set.contains()
* @param set
* @param value
* @return true if this value is not contained in the Set or Set is null
*/
protected boolean denied(List set, Object value) {
if (set != null) {
return !set.contains(value);
}
return true;
}
/**
* Budget Aggregation Code (fobj_bdgt_aggr_cd) must have an institutionally specified value
*
* @param budgetAggregationCode
* @return true if this is a legal budget aggregation code
*/
protected boolean isLegalBudgetAggregationCode(String budgetAggregationCode) {
// find all the matching records
Map whereMap = new HashMap();
whereMap.put("code", budgetAggregationCode);
Collection budgetAggregationCodes = getBoService().findMatching(BudgetAggregationCode.class, whereMap);
// if there is at least one result, then entered budget aggregation code is legal
return budgetAggregationCodes.size() > 0;
}
/**
*
* This checks to see if the object code already exists in the system
* @param year
* @param chart
* @param objectCode
* @return true if it exists
*/
protected boolean verifyObjectCode(Integer year, String chart, String objectCode) {
return null != objectCodeService.getByPrimaryId(year, chart, objectCode);
}
/**
*
* This method checks When the value of reportsToChartCode does not have an institutional exception, the Reports to Object
* (rpts_to_fin_obj_cd) fiscal year, and chart code must exist in the object code table
* if the chart and object are the same, then skip the check
* this assumes that the validity of the reports-to object code has already been tested (and corrected if necessary)
* @param year
* @param chart
* @param objectCode
* @param reportsToChartCode
* @param reportsToObjectCode
* @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
* or if the object code already exists
*/
protected boolean verifyReportsToChartCode(Integer year, String chart, String objectCode, String reportsToChartCode, String reportsToObjectCode) {
// TODO: verify this ambiguously stated rule against the UNIFACE source
// When the value of reportsToChartCode does not have an institutional exception, the Reports to Object
// (rpts_to_fin_obj_cd) fiscal year, and chart code must exist in the object code table
// if the chart and object are the same, then skip the check
// this assumes that the validity of the reports-to object code has already been tested (and corrected if necessary)
if (StringUtils.equals(reportsToChartCode, chart) && StringUtils.equals(reportsToObjectCode, objectCode)) {
return true;
}
// otherwise, check if the object is valid
return verifyObjectCode(year, reportsToChartCode, reportsToObjectCode);
}
/**
* Determines if the given maintenance document constitutes an inactivation of the object code it is maintaining
* @param maintenanceDocument the maintenance document maintaining an object code
* @return true if the document is inactivating the object code, false otherwise
*/
protected boolean isObjectCodeInactivating(MaintenanceDocument maintenanceDocument) {
if (maintenanceDocument.isEdit() && maintenanceDocument.getOldMaintainableObject() != null && maintenanceDocument.getOldMaintainableObject().getBusinessObject() != null) {
final ObjectCode oldObjectCode = (ObjectCode)maintenanceDocument.getOldMaintainableObject().getBusinessObject();
final ObjectCode newObjectCode = (ObjectCode)maintenanceDocument.getNewMaintainableObject().getBusinessObject();
return oldObjectCode.isActive() && !newObjectCode.isActive();
}
return false;
}
/**
* Checks that no offset definitions are dependent on the given object code if it is inactivated
* @param objectCode the object code trying to inactivate
* @return true if no offset definitions rely on the object code, false otherwise; this method also inserts error statements
*/
protected boolean checkForBlockingOffsetDefinitions(ObjectCode objectCode) {
final BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class);
boolean result = true;
Map<String, Object> keys = new HashMap<String, Object>();
keys.put("universityFiscalYear", objectCode.getUniversityFiscalYear());
keys.put("chartOfAccountsCode", objectCode.getChartOfAccountsCode());
keys.put("financialObjectCode", objectCode.getFinancialObjectCode());
final int matchingCount = businessObjectService.countMatching(OffsetDefinition.class, keys);
if (matchingCount > 0) {
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()});
result = false;
}
return result;
}
/**
* Checks that no ICR Exclusion by Account records are dependent on the given object code if it is inactivated
* @param objectCode the object code trying to inactivate
* @return if no ICR Exclusion by Account records rely on the object code, false otherwise; this method also inserts error statements
*/
protected boolean checkForBlockingIndirectCostRecoveryExclusionAccounts(ObjectCode objectCode) {
boolean result = true;
final UniversityDateService universityDateService = SpringContext.getBean(UniversityDateService.class);
if (objectCode.getUniversityFiscalYear() != null && objectCode.getUniversityFiscalYear().equals(universityDateService.getCurrentFiscalYear())) {
final BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class);
Map<String, Object> keys = new HashMap<String, Object>();
keys.put("chartOfAccountsCode", objectCode.getChartOfAccountsCode());
keys.put("financialObjectCode", objectCode.getFinancialObjectCode());
final int matchingCount = businessObjectService.countMatching(IndirectCostRecoveryExclusionAccount.class, keys);
if (matchingCount > 0) {
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()});
result = false;
}
}
return result;
}
}