/*
* 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.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.A21IndirectCostRecoveryAccount;
import org.kuali.kfs.coa.businessobject.A21SubAccount;
import org.kuali.kfs.coa.businessobject.IndirectCostRecoveryAccount;
import org.kuali.kfs.coa.businessobject.IndirectCostRecoveryRateDetail;
import org.kuali.kfs.coa.businessobject.SubAccount;
import org.kuali.kfs.coa.service.SubFundGroupService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.rice.kns.document.MaintenanceDocument;
import org.kuali.rice.kns.service.DataDictionaryService;
import org.kuali.rice.krad.util.ObjectUtils;
/**
* This class implements the business rules specific to the {@link SubAccount} Maintenance Document.
*/
public class SubAccountRule extends IndirectCostRecoveryAccountsRule {
protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SubAccountRule.class);
protected SubAccount oldSubAccount;
protected SubAccount newSubAccount;
/**
* This performs rules checks on document approve
* <ul>
* <li>{@link SubAccountRule#setCgAuthorized(boolean)}</li>
* <li>{@link SubAccountRule#checkForPartiallyEnteredReportingFields()}</li>
* <li>{@link SubAccountRule#checkCgRules(MaintenanceDocument)}</li>
* </ul>
* This rule fails on business rule failures
*
* @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomApproveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
*/
protected boolean processCustomApproveDocumentBusinessRules(MaintenanceDocument document) {
LOG.debug("Entering processCustomApproveDocumentBusinessRules()");
// check that all sub-objects whose keys are specified have matching objects in the db
boolean success = checkForPartiallyEnteredReportingFields();
// process CG rules if appropriate
success &= checkCgRules(document);
return success;
}
/**
* This performs rules checks on document route
* <ul>
* <li>{@link SubAccountRule#setCgAuthorized(boolean)}</li>
* <li>{@link SubAccountRule#checkForPartiallyEnteredReportingFields()}</li>
* <li>{@link SubAccountRule#checkCgRules(MaintenanceDocument)}</li>
* </ul>
* This rule fails on business rule failures
*
* @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomRouteDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
*/
protected boolean processCustomRouteDocumentBusinessRules(MaintenanceDocument document) {
LOG.debug("Entering processCustomRouteDocumentBusinessRules()");
boolean success = true;
// check that all sub-objects whose keys are specified have matching objects in the db
success &= checkForPartiallyEnteredReportingFields();
// process CG rules if appropriate
success &= checkCgRules(document);
success &= super.processCustomRouteDocumentBusinessRules(document);
return success;
}
/**
* This performs rules checks on document save
* <ul>
* <li>{@link SubAccountRule#setCgAuthorized(boolean)}</li>
* <li>{@link SubAccountRule#checkForPartiallyEnteredReportingFields()}</li>
* <li>{@link SubAccountRule#checkCgRules(MaintenanceDocument)}</li>
* </ul>
* This rule does not fail on business rule failures
*
* @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
*/
protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) {
boolean success = true;
LOG.debug("Entering processCustomSaveDocumentBusinessRules()");
// check that all sub-objects whose keys are specified have matching objects in the db
success &= checkForPartiallyEnteredReportingFields();
// process CG rules if appropriate
success &= checkCgRules(document);
return success;
}
/**
* This method sets the convenience objects like newAccount and oldAccount, so you have short and easy handles to the new and
* old objects contained in the maintenance document. It also calls the BusinessObjectBase.refresh(), which will attempt to load
* all sub-objects from the DB by their primary keys, if available.
*
* @param document - the maintenanceDocument being evaluated
*/
public void setupConvenienceObjects() {
// setup oldAccount convenience objects, make sure all possible sub-objects are populated
oldSubAccount = (SubAccount) super.getOldBo();
refreshSubObjects(oldSubAccount);
// setup newAccount convenience objects, make sure all possible sub-objects are populated
newSubAccount = (SubAccount) super.getNewBo();
refreshSubObjects(newSubAccount);
//icr rule checking setup
if (newSubAccount.getA21SubAccount() != null){
List<IndirectCostRecoveryAccount> icrAccountList = new ArrayList<IndirectCostRecoveryAccount>(
newSubAccount.getA21SubAccount().getA21ActiveIndirectCostRecoveryAccounts());
setActiveIndirectCostRecoveryAccountList(icrAccountList);
setBoFieldPath(KFSPropertyConstants.A21INDIRECT_COST_RECOVERY_ACCOUNTS);
}
}
/**
* Refreshes the references of account
*
* @param subaccount SubAccount
*/
void refreshSubObjects(SubAccount subaccount) {
if (subaccount != null) {
if (subaccount.getA21SubAccount() != null) {
subaccount.getA21SubAccount().refreshNonUpdateableReferences();
// refresh contacts
// if (subaccount.getA21SubAccount().getA21IndirectCostRecoveryAccounts() != null) {
// for (A21IndirectCostRecoveryAccount icra : subaccount.getA21SubAccount().getA21IndirectCostRecoveryAccounts()) {
// icra.refreshNonUpdateableReferences();
// }
// }
}
}
}
/**
* This checks that the reporting fields are entered altogether or none at all
*
* @return false if only one reporting field filled out and not all of them, true otherwise
*/
protected boolean checkForPartiallyEnteredReportingFields() {
LOG.debug("Entering checkExistenceAndActive()");
boolean success = true;
boolean allReportingFieldsEntered = false;
boolean anyReportingFieldsEntered = false;
// set a flag if all three reporting fields are filled (this is separated just for readability)
if (StringUtils.isNotEmpty(newSubAccount.getFinancialReportChartCode()) && StringUtils.isNotEmpty(newSubAccount.getFinReportOrganizationCode()) && StringUtils.isNotEmpty(newSubAccount.getFinancialReportingCode())) {
allReportingFieldsEntered = true;
}
// set a flag if any of the three reporting fields are filled (this is separated just for readability)
if (StringUtils.isNotEmpty(newSubAccount.getFinancialReportChartCode()) || StringUtils.isNotEmpty(newSubAccount.getFinReportOrganizationCode()) || StringUtils.isNotEmpty(newSubAccount.getFinancialReportingCode())) {
anyReportingFieldsEntered = true;
}
// if any of the three reporting code fields are filled out, all three must be, or none
// if any of the three are entered
if (anyReportingFieldsEntered && !allReportingFieldsEntered) {
putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_RPTCODE_ALL_FIELDS_IF_ANY_FIELDS);
success &= false;
}
return success;
}
/**
* This checks to make sure that if cgAuthorized is false it succeeds immediately, otherwise it checks that all the information
* for CG is correctly entered and identified including:
* <ul>
* <li>If the {@link SubFundGroup} isn't for Contracts and Grants then check to make sure that the cost share and ICR fields are
* not empty</li>
* <li>If it isn't a child of CG, then the SubAccount must be of type ICR</li>
* </ul>
*
* @param document
* @return true if the user is not authorized to change CG fields, otherwise it checks the above conditions
*/
protected boolean checkCgRules(MaintenanceDocument document) {
boolean success = true;
// short circuit if the parent account is NOT part of a CG fund group
boolean a21SubAccountRefreshed = false;
if (ObjectUtils.isNotNull(newSubAccount.getAccount())) {
if (ObjectUtils.isNotNull(newSubAccount.getAccount().getSubFundGroup())) {
// compare them, exit if the account isn't for contracts and grants
if (!SpringContext.getBean(SubFundGroupService.class).isForContractsAndGrants(newSubAccount.getAccount().getSubFundGroup())) {
// KULCOA-1116 - Check if CG CS and CG ICR are empty, if not throw an error
if (checkCgCostSharingIsEmpty() == false) {
putFieldError("a21SubAccount.costShareChartOfAccountCode", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NON_FUNDED_ACCT_CS_INVALID, new String[] { SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingAttributeLabel(), SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingValueForMessage() });
success = false;
}
if (checkCgIcrIsEmpty() == false) {
putFieldError("a21SubAccount.indirectCostRecoveryTypeCode", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NON_FUNDED_ACCT_ICR_INVALID, new String[] { SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingAttributeLabel(), SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingValueForMessage() });
success = false;
}
// KULRNE-4660 - this isn't the child of a CG account; sub account must be ICR type
if (!ObjectUtils.isNull(newSubAccount.getA21SubAccount())) {
// KFSMI-798 - refresh() changed to refreshNonUpdateableReferences()
// All references for A21SubAccount are non-updatable
newSubAccount.getA21SubAccount().refreshNonUpdateableReferences();
a21SubAccountRefreshed = true;
if (StringUtils.isEmpty(newSubAccount.getA21SubAccount().getSubAccountTypeCode()) || !newSubAccount.getA21SubAccount().getSubAccountTypeCode().equals(KFSConstants.SubAccountType.EXPENSE)) {
putFieldError("a21SubAccount.subAccountTypeCode", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NON_FUNDED_ACCT_SUB_ACCT_TYPE_CODE_INVALID, new String[] { SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingAttributeLabel(), SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingValueForMessage() });
success = false;
}
}
return success;
}
}
}
A21SubAccount a21 = newSubAccount.getA21SubAccount();
// short circuit if there is no A21SubAccount object at all (ie, null)
if (ObjectUtils.isNull(a21)) {
return success;
}
// FROM HERE ON IN WE CAN ASSUME THERE IS A VALID A21 SUBACCOUNT OBJECT
// KFSMI-6848 since there is a ICR Collection Account object, change refresh to perform
// manually refresh the a21SubAccount object, as it wont have been
// refreshed by the parent, as its updateable
// though only refresh if we didn't refresh in the checks above
if (!a21SubAccountRefreshed) {
//preserve the ICRAccounts before refresh to prevent the list from dropping
List<A21IndirectCostRecoveryAccount>icrAccounts =a21.getA21IndirectCostRecoveryAccounts();
a21.refresh();
a21.setA21IndirectCostRecoveryAccounts(icrAccounts);
}
// C&G A21 Type field must be in the allowed values
if (!KFSConstants.SubAccountType.ELIGIBLE_SUB_ACCOUNT_TYPE_CODES.contains(a21.getSubAccountTypeCode())) {
putFieldError("a21SubAccount.subAccountTypeCode", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_INVALI_SUBACCOUNT_TYPE_CODES, KFSConstants.SubAccountType.ELIGIBLE_SUB_ACCOUNT_TYPE_CODES.toString());
success &= false;
}
// get a convenience reference to this code
String cgA21TypeCode = a21.getSubAccountTypeCode();
// if this is a Cost Sharing SubAccount, run the Cost Sharing rules
if (KFSConstants.SubAccountType.COST_SHARE.trim().equalsIgnoreCase(StringUtils.trim(cgA21TypeCode))) {
success &= checkCgCostSharingRules();
}
// if this is an ICR subaccount, run the ICR rules
if (KFSConstants.SubAccountType.EXPENSE.trim().equals(StringUtils.trim(cgA21TypeCode))) {
success &= checkCgIcrRules();
}
return success;
}
/**
* This checks that if the cost share information is filled out that it is valid and exists, or if fields are missing (such as
* the chart of accounts code and account number) an error is recorded
*
* @return true if all cost share fields filled out correctly, false if the chart of accounts code and account number for cost
* share are missing
*/
protected boolean checkCgCostSharingRules() {
boolean success = true;
boolean allFieldsSet = false;
A21SubAccount a21 = newSubAccount.getA21SubAccount();
// check to see if all required fields are set
if (StringUtils.isNotEmpty(a21.getCostShareChartOfAccountCode()) && StringUtils.isNotEmpty(a21.getCostShareSourceAccountNumber())) {
allFieldsSet = true;
}
// Cost Sharing COA Code and Cost Sharing Account Number are required
success &= checkEmptyBOField("a21SubAccount.costShareChartOfAccountCode", a21.getCostShareChartOfAccountCode(), "Cost Share Chart of Accounts Code");
success &= checkEmptyBOField("a21SubAccount.costShareSourceAccountNumber", a21.getCostShareSourceAccountNumber(), "Cost Share AccountNumber");
// existence test on Cost Share Account
if (allFieldsSet) {
if (ObjectUtils.isNull(a21.getCostShareAccount())) {
putFieldError("a21SubAccount.costShareSourceAccountNumber", KFSKeyConstants.ERROR_EXISTENCE, getDisplayName("a21SubAccount.costShareSourceAccountNumber"));
success &= false;
}
}
// existence test on Cost Share SubAccount
if (allFieldsSet && StringUtils.isNotBlank(a21.getCostShareSourceSubAccountNumber())) {
if (ObjectUtils.isNull(a21.getCostShareSourceSubAccount())) {
putFieldError("a21SubAccount.costShareSourceSubAccountNumber", KFSKeyConstants.ERROR_EXISTENCE, getDisplayName("a21SubAccount.costShareSourceSubAccountNumber"));
success &= false;
}
}
// Cost Sharing Account may not be for contracts and grants
if (ObjectUtils.isNotNull(a21.getCostShareAccount())) {
if (ObjectUtils.isNotNull(a21.getCostShareAccount().getSubFundGroup())) {
if (a21.getCostShareAccount().isForContractsAndGrants()) {
putFieldError("a21SubAccount.costShareSourceAccountNumber", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_COST_SHARE_ACCOUNT_MAY_NOT_BE_CG_FUNDGROUP, new String[] { SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingAttributeLabel(), SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingValueForMessage() });
success &= false;
}
}
}
// The ICR fields must be empty if the sub-account type code is for cost sharing
if (checkCgIcrIsEmpty() == false) {
putFieldError("a21SubAccount.indirectCostRecoveryTypeCode", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_ICR_SECTION_INVALID, a21.getSubAccountTypeCode());
success &= false;
}
return success;
}
/**
* This checks that if the ICR information is entered that it is valid for this fiscal year and that all of its fields are valid
* as well (such as account)
*
* @return true if the ICR information is filled in and it is valid
*/
protected boolean checkCgIcrRules() {
A21SubAccount a21 = newSubAccount.getA21SubAccount();
if(ObjectUtils.isNull(a21)) {
return true;
}
boolean success = true;
// existence check for ICR Type Code
if (StringUtils.isNotEmpty(a21.getIndirectCostRecoveryTypeCode())) {
if (ObjectUtils.isNull(a21.getIndirectCostRecoveryType())) {
putFieldError("a21SubAccount.indirectCostRecoveryTypeCode", KFSKeyConstants.ERROR_EXISTENCE, "ICR Type Code: " + a21.getIndirectCostRecoveryTypeCode());
success = false;
}
}
// existence check for Financial Series ID
if (StringUtils.isNotEmpty(a21.getFinancialIcrSeriesIdentifier())) {
String fiscalYear = StringUtils.EMPTY + SpringContext.getBean(UniversityDateService.class).getCurrentFiscalYear();
String icrSeriesId = a21.getFinancialIcrSeriesIdentifier();
Map<String, String> pkMap = new HashMap<String, String>();
pkMap.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, fiscalYear);
pkMap.put(KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, icrSeriesId);
Collection<IndirectCostRecoveryRateDetail> icrRateDetails = getBoService().findMatching(IndirectCostRecoveryRateDetail.class, pkMap);
if (ObjectUtils.isNull(icrRateDetails) || icrRateDetails.isEmpty()) {
String label = SpringContext.getBean(DataDictionaryService.class).getAttributeLabel(A21SubAccount.class, KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER);
putFieldError(KFSPropertyConstants.A21_SUB_ACCOUNT + "." + KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, KFSKeyConstants.ERROR_EXISTENCE, label + " (" + icrSeriesId + ")");
success = false;
}
else {
for(IndirectCostRecoveryRateDetail icrRateDetail : icrRateDetails) {
if(ObjectUtils.isNull(icrRateDetail.getIndirectCostRecoveryRate())){
putFieldError(KFSPropertyConstants.A21_SUB_ACCOUNT + "." + KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, KFSKeyConstants.IndirectCostRecovery.ERROR_DOCUMENT_ICR_RATE_NOT_FOUND, new String[]{fiscalYear, icrSeriesId});
success = false;
break;
}
}
}
}
// existence check for ICR Account
for (A21IndirectCostRecoveryAccount account : a21.getA21ActiveIndirectCostRecoveryAccounts()){
if (StringUtils.isNotBlank(account.getIndirectCostRecoveryAccountNumber())
&& StringUtils.isNotBlank(account.getIndirectCostRecoveryFinCoaCode())){
if(ObjectUtils.isNull(account.getIndirectCostRecoveryAccount())){
putFieldError(KFSPropertyConstants.A21INDIRECT_COST_RECOVERY_ACCOUNTS, KFSKeyConstants.ERROR_EXISTENCE, "ICR Account: " + account.getIndirectCostRecoveryFinCoaCode() + "-" + account.getIndirectCostRecoveryAccountNumber());
success = false;
break;
}
}
}
// The cost sharing fields must be empty if the sub-account type code is for ICR
if (checkCgCostSharingIsEmpty() == false) {
putFieldError("a21SubAccount.costShareChartOfAccountCode", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_COST_SHARE_SECTION_INVALID, a21.getSubAccountTypeCode());
success &= false;
}
return success;
}
/**
* This method tests if all fields in the Cost Sharing section are empty.
*
* @return true if the cost sharing values passed in are empty, otherwise false.
*/
protected boolean checkCgCostSharingIsEmpty() {
boolean success = true;
A21SubAccount newA21SubAccount = newSubAccount.getA21SubAccount();
if (ObjectUtils.isNotNull(newA21SubAccount)) {
success &= StringUtils.isEmpty(newA21SubAccount.getCostShareChartOfAccountCode());
success &= StringUtils.isEmpty(newA21SubAccount.getCostShareSourceAccountNumber());
success &= StringUtils.isEmpty(newA21SubAccount.getCostShareSourceSubAccountNumber());
}
return success;
}
/**
* This method tests if all fields in the ICR section are empty.
*
* @return true if the ICR values passed in are empty, otherwise false.
*/
protected boolean checkCgIcrIsEmpty() {
boolean success = true;
A21SubAccount newA21SubAccount = newSubAccount.getA21SubAccount();
if (ObjectUtils.isNotNull(newA21SubAccount)) {
success &= StringUtils.isEmpty(newA21SubAccount.getFinancialIcrSeriesIdentifier());
success &= checkICRCollectionExist(false);
success &= StringUtils.isEmpty(newA21SubAccount.getIndirectCostRecoveryTypeCode());
// this is a boolean, so create any value if set to true, meaning a user checked the box, otherwise assume it's empty
success &= StringUtils.isEmpty(newA21SubAccount.getOffCampusCode() ? "1" : "");
}
return success;
}
/**
* This method tests the value entered, and if there is anything there it logs a new error, and returns false.
*
* @param value - String value to be tested
* @param fieldName - name of the field being tested
* @return false if there is any value in value, otherwise true
*/
protected boolean disallowAnyValues(String value, String fieldName) {
if (StringUtils.isNotEmpty(value)) {
putFieldError(fieldName, KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NOT_AUTHORIZED_ENTER_CG_FIELDS, getDisplayName(fieldName));
return false;
}
return true;
}
/**
* This method tests the two values entered, and if there is any change between the two, it logs an error, and returns false.
* Note that the comparison is done after trimming both leading and trailing whitespace from both strings, and then doing a
* case-insensitive comparison.
*
* @param oldValue - the original String value of the field
* @param newValue - the new String value of the field
* @param fieldName - name of the field being tested
* @return false if there is any difference between the old and new, true otherwise
*/
protected boolean disallowChangedValues(String oldValue, String newValue, String fieldName) {
if (isFieldValueChanged(oldValue, newValue)) {
putFieldError(fieldName, KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NOT_AUTHORIZED_CHANGE_CG_FIELDS, getDisplayName(fieldName));
return false;
}
return true;
}
/**
* This compares two string values to see if the newValue has changed from the oldValue
*
* @param oldValue - original value
* @param newValue - new value
* @return true if the two fields are different from each other
*/
protected boolean isFieldValueChanged(String oldValue, String newValue) {
if (StringUtils.isBlank(oldValue) && StringUtils.isBlank(newValue)) {
return false;
}
if (StringUtils.isBlank(oldValue) && StringUtils.isNotBlank(newValue)) {
return true;
}
if (StringUtils.isNotBlank(oldValue) && StringUtils.isBlank(newValue)) {
return true;
}
if (!oldValue.trim().equalsIgnoreCase(newValue.trim())) {
return true;
}
return false;
}
/**
* This method retrieves the label name for a specific property
*
* @param propertyName - property to retrieve label for (from the DD)
* @return the label
*/
protected String getDisplayName(String propertyName) {
return getDdService().getAttributeLabel(SubAccount.class, propertyName);
}
}