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.module.bc.document.validation.impl;
017
018 import java.beans.PropertyDescriptor;
019 import java.lang.reflect.InvocationTargetException;
020 import java.text.SimpleDateFormat;
021 import java.util.Arrays;
022 import java.util.Calendar;
023 import java.util.Collections;
024 import java.util.HashMap;
025 import java.util.Iterator;
026 import java.util.List;
027 import java.util.Map;
028
029 import org.apache.commons.beanutils.PropertyUtils;
030 import org.apache.commons.lang.StringUtils;
031 import org.kuali.kfs.coa.businessobject.A21SubAccount;
032 import org.kuali.kfs.coa.businessobject.Account;
033 import org.kuali.kfs.coa.businessobject.ObjectCode;
034 import org.kuali.kfs.coa.businessobject.SubAccount;
035 import org.kuali.kfs.coa.businessobject.SubObjectCode;
036 import org.kuali.kfs.fp.service.FiscalYearFunctionControlService;
037 import org.kuali.kfs.module.bc.BCConstants;
038 import org.kuali.kfs.module.bc.BCKeyConstants;
039 import org.kuali.kfs.module.bc.BCPropertyConstants;
040 import org.kuali.kfs.module.bc.BCConstants.AccountSalarySettingOnlyCause;
041 import org.kuali.kfs.module.bc.BCConstants.MonthSpreadDeleteType;
042 import org.kuali.kfs.module.bc.businessobject.BudgetConstructionMonthly;
043 import org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionGeneralLedger;
044 import org.kuali.kfs.module.bc.document.BudgetConstructionDocument;
045 import org.kuali.kfs.module.bc.document.service.BenefitsCalculationService;
046 import org.kuali.kfs.module.bc.document.service.BudgetDocumentService;
047 import org.kuali.kfs.module.bc.document.service.BudgetParameterService;
048 import org.kuali.kfs.module.bc.document.service.SalarySettingService;
049 import org.kuali.kfs.module.bc.document.validation.AddBudgetConstructionDocumentRule;
050 import org.kuali.kfs.module.bc.document.validation.AddPendingBudgetGeneralLedgerLineRule;
051 import org.kuali.kfs.module.bc.document.validation.DeleteMonthlySpreadRule;
052 import org.kuali.kfs.module.bc.document.validation.DeletePendingBudgetGeneralLedgerLineRule;
053 import org.kuali.kfs.module.bc.document.validation.SaveMonthlyBudgetRule;
054 import org.kuali.kfs.module.bc.util.BudgetParameterFinder;
055 import org.kuali.kfs.sys.KFSConstants;
056 import org.kuali.kfs.sys.KFSKeyConstants;
057 import org.kuali.kfs.sys.KFSPropertyConstants;
058 import org.kuali.kfs.sys.context.SpringContext;
059 import org.kuali.kfs.sys.document.service.AccountingLineRuleHelperService;
060 import org.kuali.rice.kns.datadictionary.DataDictionary;
061 import org.kuali.rice.kns.document.Document;
062 import org.kuali.rice.kns.exception.InfrastructureException;
063 import org.kuali.rice.kns.rules.TransactionalDocumentRuleBase;
064 import org.kuali.rice.kns.service.BusinessObjectService;
065 import org.kuali.rice.kns.service.DataDictionaryService;
066 import org.kuali.rice.kns.service.PersistenceService;
067 import org.kuali.rice.kns.util.ErrorMap;
068 import org.kuali.rice.kns.util.GlobalVariables;
069 import org.kuali.rice.kns.util.KNSConstants;
070 import org.kuali.rice.kns.util.KualiInteger;
071 import org.kuali.rice.kns.util.MessageMap;
072 import org.kuali.rice.kns.util.ObjectUtils;
073 import org.kuali.rice.kns.util.TypeUtils;
074
075 public class BudgetConstructionDocumentRules extends TransactionalDocumentRuleBase implements AddBudgetConstructionDocumentRule<BudgetConstructionDocument>, AddPendingBudgetGeneralLedgerLineRule<BudgetConstructionDocument, PendingBudgetConstructionGeneralLedger>, DeletePendingBudgetGeneralLedgerLineRule<BudgetConstructionDocument, PendingBudgetConstructionGeneralLedger>, DeleteMonthlySpreadRule<BudgetConstructionDocument>, SaveMonthlyBudgetRule<BudgetConstructionDocument, BudgetConstructionMonthly> {
076 protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BudgetConstructionDocumentRules.class);
077
078 // some services used here - other service refs are from parent classes
079 // if this class is extended we may need to create protected getters
080 protected static BudgetParameterService budgetParameterService = SpringContext.getBean(BudgetParameterService.class);
081 protected static AccountingLineRuleHelperService accountingLineRuleHelper = SpringContext.getBean(AccountingLineRuleHelperService.class);
082 protected static DataDictionaryService dataDictionaryService = SpringContext.getBean(DataDictionaryService.class);
083 protected static SalarySettingService salarySettingService = SpringContext.getBean(SalarySettingService.class);
084 protected static BusinessObjectService businessObjectService = SpringContext.getBean(BusinessObjectService.class);
085 protected static FiscalYearFunctionControlService fiscalYearFunctionControlService = SpringContext.getBean(FiscalYearFunctionControlService.class);
086
087 protected List<String> revenueObjectTypesParamValues = BudgetParameterFinder.getRevenueObjectTypes();
088 protected List<String> expenditureObjectTypesParamValues = BudgetParameterFinder.getExpenditureObjectTypes();
089 protected List<String> budgetAggregationCodesParamValues = BudgetParameterFinder.getBudgetAggregationCodes();
090 protected List<String> fringeBenefitDesignatorCodesParamValues = BudgetParameterFinder.getFringeBenefitDesignatorCodes();
091 protected List<String> salarySettingFundGroupsParamValues = BudgetParameterFinder.getSalarySettingFundGroups();
092 protected List<String> salarySettingSubFundGroupsParamValues = BudgetParameterFinder.getSalarySettingSubFundGroups();
093
094 // this field is highlighted for any errors found on an existing line
095 protected static final String TARGET_ERROR_PROPERTY_NAME = KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT;
096
097 public BudgetConstructionDocumentRules() {
098 super();
099 }
100
101 /**
102 * @see org.kuali.kfs.module.bc.document.validation.AddBudgetConstructionDocumentRule#processAddBudgetConstructionDocumentRules(org.kuali.kfs.module.bc.document.BudgetConstructionDocument)
103 */
104 public boolean processAddBudgetConstructionDocumentRules(BudgetConstructionDocument budgetConstructionDocument) {
105 LOG.debug("processAddBudgetConstructionDocumentRules(Document) - start");
106
107 MessageMap errors = GlobalVariables.getMessageMap();
108 boolean isValid = true;
109
110 // validate primitives for required field and formatting checks
111 int originalErrorCount = errors.getErrorCount();
112 getDictionaryValidationService().validateBusinessObject(budgetConstructionDocument);
113
114 // check to see if any errors were reported
115 int currentErrorCount = errors.getErrorCount();
116 isValid &= (currentErrorCount == originalErrorCount);
117 if (!isValid) {
118 return isValid;
119 }
120
121 // can't create BC documents when in system view only mode
122 // let the user know this up front
123 if (!fiscalYearFunctionControlService.isBudgetUpdateAllowed(budgetConstructionDocument.getUniversityFiscalYear())) {
124 errors.putError(KFSPropertyConstants.ACCOUNT_NUMBER, BCKeyConstants.MESSAGE_BUDGET_SYSTEM_VIEW_ONLY);
125 isValid &= false;
126 }
127
128 // check existence of account first
129 DataDictionary dd = dataDictionaryService.getDataDictionary();
130 String pkeyValue = budgetConstructionDocument.getChartOfAccountsCode() + "-" + budgetConstructionDocument.getAccountNumber();
131 isValid &= isValidAccount(budgetConstructionDocument.getAccount(), pkeyValue, dd, KFSPropertyConstants.ACCOUNT_NUMBER);
132 if (isValid) {
133
134 // run the rules checks preventing BC document creation - assumes account exists
135 isValid &= this.isBudgetAllowed(budgetConstructionDocument, KFSPropertyConstants.ACCOUNT_NUMBER, errors, true, true);
136 }
137
138 if (!isValid) {
139
140 // tell the user we can't create a new BC document along with the error reasons
141 GlobalVariables.getMessageList().add(BCKeyConstants.MESSAGE_BUDGET_NOCREATE_DOCUMENT);
142 }
143
144 LOG.debug("processAddBudgetConstructionDocumentRules(Document) - end");
145 return isValid;
146 }
147
148 /**
149 * Runs business rules prior to saving Budget Document proper. This is different than saving typical KFS documents in that the
150 * document is not saved to the user's inbox. Saved Budget Documents must meet the same state requirements as the typical KFS
151 * routed document, so required field checks must be done. Budget Documents can be opened by a user in edit mode multiple times
152 * and while in edit mode documents can be pushed down the review hierarchy, monthly budgets and appointment funding updated,
153 * benefits calculated, etc. Each of these operations require the document's data be in a consistent state with respect to
154 * business rules before the operation be performed.
155 *
156 * @see org.kuali.rice.kns.rules.DocumentRuleBase#processSaveDocument(org.kuali.rice.kns.document.Document)
157 */
158 @Override
159 public boolean processSaveDocument(Document document) {
160 LOG.debug("processSaveDocument(Document) - start");
161
162 boolean isValid = true;
163
164 // run through the attributes recursively and check dd stuff
165 isValid &= isDocumentAttributesValid(document, true);
166
167 if (isValid) {
168 isValid &= processSaveBudgetDocumentRules((BudgetConstructionDocument) document, MonthSpreadDeleteType.NONE);
169 }
170
171 // no custom save rules since we are overriding and doing what we want here already
172
173 LOG.debug("processSaveDocument(Document) - end");
174 return isValid;
175 }
176
177 public boolean processDeleteMonthlySpreadRules(BudgetConstructionDocument budgetConstructionDocument, MonthSpreadDeleteType monthSpreadDeleteType) {
178 LOG.debug("processDeleteRevenueMonthlySpreadRules(Document) - start");
179
180 boolean isValid = true;
181
182 // run through the attributes recursively and check dd stuff
183 isValid &= isDocumentAttributesValid(budgetConstructionDocument, true);
184
185 if (isValid) {
186 isValid &= processSaveBudgetDocumentRules(budgetConstructionDocument, monthSpreadDeleteType);
187 }
188
189 // no custom save rules since we are overriding and doing what we want here already
190
191 LOG.debug("processDeleteRevenueMonthlySpreadRules(Document) - end");
192 return isValid;
193
194 }
195
196 /**
197 * Iterates through existing revenue and expenditure lines to do validation, ri checks on object/sub-object code and request
198 * amount referential integrity checks against appointment funding and monthly detail amounts. Checks are performed when the
199 * request amount has been updated, since initial add action, the last save event or since opening the document, whatever is
200 * latest.
201 *
202 * @see org.kuali.module.budget.rule.SaveBudgetDocumentRule#processSaveBudgetDocumentRules(D)
203 */
204 public boolean processSaveBudgetDocumentRules(BudgetConstructionDocument budgetConstructionDocument, MonthSpreadDeleteType monthSpreadDeleteType) {
205
206 MessageMap errors = GlobalVariables.getMessageMap();
207 boolean doRevMonthRICheck = true;
208 boolean doExpMonthRICheck = true;
209 boolean isValid = true;
210 int originalErrorCount;
211 int currentErrorCount;
212
213 // refresh only the doc refs we need
214 List refreshFields = Collections.unmodifiableList(Arrays.asList(new String[] { KFSPropertyConstants.ACCOUNT, KFSPropertyConstants.SUB_ACCOUNT }));
215 SpringContext.getBean(PersistenceService.class).retrieveReferenceObjects(budgetConstructionDocument, refreshFields);
216
217 errors.addToErrorPath(KNSConstants.DOCUMENT_PROPERTY_NAME);
218
219 if (monthSpreadDeleteType == MonthSpreadDeleteType.REVENUE) {
220 doRevMonthRICheck = false;
221 doExpMonthRICheck = true;
222 }
223 else {
224 if (monthSpreadDeleteType == MonthSpreadDeleteType.EXPENDITURE) {
225 doRevMonthRICheck = true;
226 doExpMonthRICheck = false;
227 }
228 }
229
230 // iterate and validate revenue lines
231 isValid &= this.checkPendingBudgetConstructionGeneralLedgerLines(budgetConstructionDocument, errors, true, doRevMonthRICheck);
232
233 // iterate and validate expenditure lines
234 isValid &= this.checkPendingBudgetConstructionGeneralLedgerLines(budgetConstructionDocument, errors, false, doExpMonthRICheck);
235
236 errors.removeFromErrorPath(KNSConstants.DOCUMENT_PROPERTY_NAME);
237
238 return isValid;
239 }
240
241 /**
242 * Checks a new PBGL line. Comprehensive checks are done.
243 *
244 * @param budgetConstructionDocument
245 * @param pendingBudgetConstructionGeneralLedger
246 * @param isRevenue
247 * @return
248 */
249 public boolean processAddPendingBudgetGeneralLedgerLineRules(BudgetConstructionDocument budgetConstructionDocument, PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger, boolean isRevenue) {
250 LOG.debug("processAddPendingBudgetGeneralLedgerLineRules() start");
251
252 // List refreshFields;
253 MessageMap errors = GlobalVariables.getMessageMap();
254 boolean isValid = true;
255
256 int originalErrorCount = errors.getErrorCount();
257
258 // validate primitives for required field and formatting checks
259 getDictionaryValidationService().validateBusinessObject(pendingBudgetConstructionGeneralLedger);
260
261 // check to see if any errors were reported
262 int currentErrorCount = errors.getErrorCount();
263 isValid &= (currentErrorCount == originalErrorCount);
264
265 if (isValid) {
266
267 // refresh only the doc refs we need
268 List refreshFields = Collections.unmodifiableList(Arrays.asList(new String[] { KFSPropertyConstants.ACCOUNT, KFSPropertyConstants.SUB_ACCOUNT }));
269 SpringContext.getBean(PersistenceService.class).retrieveReferenceObjects(budgetConstructionDocument, refreshFields);
270 // budgetConstructionDocument.getSubAccount().refreshReferenceObject(KFSPropertyConstants.A21_SUB_ACCOUNT);
271
272 isValid &= this.checkPendingBudgetConstructionGeneralLedgerLine(budgetConstructionDocument, pendingBudgetConstructionGeneralLedger, errors, isRevenue, true);
273
274
275 if (isValid) {
276 // line checks ok - does line already exist in target revenue or expenditure list
277 isValid &= isNewLineUnique(budgetConstructionDocument, pendingBudgetConstructionGeneralLedger, errors, isRevenue);
278 }
279 }
280
281 if (!isValid) {
282 LOG.info("business rule checks failed in processAddPendingBudgetGeneralLedgerLineRules in BudgetConstructionRules");
283 }
284
285 LOG.debug("processAddPendingBudgetGeneralLedgerLineRules() end");
286 return isValid;
287 }
288
289 /**
290 * Runs rules for deleting an existing revenue or expenditure line.
291 *
292 * @param budgetConstructionDocument
293 * @param pendingBudgetConstructionGeneralLedger
294 * @param isRevenue
295 * @return
296 */
297 public boolean processDeletePendingBudgetGeneralLedgerLineRules(BudgetConstructionDocument budgetConstructionDocument, PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger, boolean isRevenue) {
298 LOG.debug("processDeletePendingBudgetGeneralLedgerLineRules() start");
299
300 MessageMap errors = GlobalVariables.getMessageMap();
301 boolean isValid = true;
302
303 // no delete allowed if base exists, the delete button shouldn't even exist in this case, but checking anyway
304 if (pendingBudgetConstructionGeneralLedger.getFinancialBeginningBalanceLineAmount().isZero()) {
305 isValid &= true;
306 }
307 else {
308 isValid &= false;
309 String pkeyVal = pendingBudgetConstructionGeneralLedger.getFinancialObjectCode() + "," + pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode();
310 GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT, BCKeyConstants.ERROR_NO_DELETE_ALLOWED_WITH_BASE, pkeyVal);
311 }
312
313 if (!isRevenue) {
314 // no lines using fringe benefit target object codes allowed to be manually deleted by user
315 // the lines are created by benefits calculation process
316 // again the delete button shouldn't even exist
317 isValid &= isNotFringeBenefitObject(fringeBenefitDesignatorCodesParamValues, pendingBudgetConstructionGeneralLedger, errors, false);
318
319 // no deletion if salary setting option is turned on
320 // and the line is a salary detail line and detail recs exist
321 if (!SpringContext.getBean(SalarySettingService.class).isSalarySettingDisabled()) {
322 if (pendingBudgetConstructionGeneralLedger.getLaborObject() != null) {
323 if (pendingBudgetConstructionGeneralLedger.getLaborObject().isDetailPositionRequiredIndicator()) {
324 if (pendingBudgetConstructionGeneralLedger.isPendingBudgetConstructionAppointmentFundingExists()) {
325 isValid &= false;
326 String pkeyVal = pendingBudgetConstructionGeneralLedger.getFinancialObjectCode() + "," + pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode();
327 GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT, BCKeyConstants.ERROR_NO_DELETE_ALLOWED_SALARY_DETAIL, pkeyVal);
328 }
329 }
330 }
331 }
332 if (!SpringContext.getBean(BenefitsCalculationService.class).isBenefitsCalculationDisabled()) {
333
334 // benefits calc is turned on, if the line is valid to remove and the request is not zero, set to calc benefits
335 if (isValid && pendingBudgetConstructionGeneralLedger.getPositionObjectBenefit() != null && !pendingBudgetConstructionGeneralLedger.getPositionObjectBenefit().isEmpty()) {
336 budgetConstructionDocument.setBenefitsCalcNeeded(true);
337
338 // test if the line has monthly budgets
339 // this assumes business rule of non-zero monthly budget not allowed to sum to a zero annual amount
340 // that is, if annual amount is zero, the monthly record contains all zeros
341 if (pendingBudgetConstructionGeneralLedger.getBudgetConstructionMonthly() != null && !pendingBudgetConstructionGeneralLedger.getBudgetConstructionMonthly().isEmpty()) {
342 budgetConstructionDocument.setMonthlyBenefitsCalcNeeded(true);
343 }
344 }
345 }
346 }
347
348 LOG.debug("processDeletePendingBudgetGeneralLedgerLineRules() end");
349 return isValid;
350 }
351
352 /**
353 * @see org.kuali.kfs.module.bc.document.validation.SaveMonthlyBudgetRule#processSaveMonthlyBudgetRules(org.kuali.kfs.module.bc.document.BudgetConstructionDocument,
354 * org.kuali.kfs.module.bc.businessobject.BudgetConstructionMonthly)
355 */
356 public boolean processSaveMonthlyBudgetRules(BudgetConstructionDocument budgetConstructionDocument, BudgetConstructionMonthly budgetConstructionMonthly) {
357 LOG.debug("processSaveMonthlyBudgetRules() start");
358
359 budgetConstructionMonthly.refreshReferenceObject("pendingBudgetConstructionGeneralLedger");
360 PendingBudgetConstructionGeneralLedger pbgl = budgetConstructionMonthly.getPendingBudgetConstructionGeneralLedger();
361 MessageMap errors = GlobalVariables.getMessageMap();
362 boolean isValid = true;
363
364 int originalErrorCount = errors.getErrorCount();
365
366 // validate primitives for required field and formatting checks
367 getDictionaryValidationService().validateBusinessObject(budgetConstructionMonthly);
368
369 // check to see if any errors were reported
370 int currentErrorCount = errors.getErrorCount();
371 isValid &= (currentErrorCount == originalErrorCount);
372
373 // Check special cleanup mode case and berate user on save of anything.
374 // The user should delete the row, which bypasses this rule.
375 if (!budgetConstructionDocument.isBudgetableDocument()) {
376 isValid &= Boolean.FALSE;
377 errors.putError(BCPropertyConstants.FINANCIAL_DOCUMENT_MONTH1_LINE_AMOUNT, BCKeyConstants.ERROR_BUDGET_DOCUMENT_NOT_BUDGETABLE, budgetConstructionDocument.getAccountNumber() + ";" + budgetConstructionDocument.getSubAccountNumber());
378 }
379
380 if (isValid) {
381 KualiInteger monthlyTotal = budgetConstructionMonthly.getFinancialDocumentMonthTotalLineAmount();
382 if (!salarySettingService.isSalarySettingDisabled()) {
383 if (pbgl.getLaborObject() != null && pbgl.getLaborObject().isDetailPositionRequiredIndicator()) {
384
385 // no request amount overrides allowed for salary setting detail lines
386 if (!monthlyTotal.equals(pbgl.getAccountLineAnnualBalanceAmount())) {
387 isValid &= false;
388 errors.putError(BCPropertyConstants.FINANCIAL_DOCUMENT_MONTH1_LINE_AMOUNT, BCKeyConstants.ERROR_MONTHLY_DETAIL_SALARY_OVERIDE, budgetConstructionMonthly.getFinancialObjectCode(), monthlyTotal.toString(), pbgl.getAccountLineAnnualBalanceAmount().toString());
389 }
390 }
391 }
392
393 // check for monthly total adding to zero (makes no sense)
394 if (monthlyTotal.isZero()) {
395 boolean nonZeroMonthlyExists = false;
396 nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth1LineAmount().isNonZero();
397 nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth2LineAmount().isNonZero();
398 nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth3LineAmount().isNonZero();
399 nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth4LineAmount().isNonZero();
400 nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth5LineAmount().isNonZero();
401 nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth6LineAmount().isNonZero();
402 nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth7LineAmount().isNonZero();
403 nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth8LineAmount().isNonZero();
404 nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth9LineAmount().isNonZero();
405 nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth10LineAmount().isNonZero();
406 nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth11LineAmount().isNonZero();
407 nonZeroMonthlyExists |= budgetConstructionMonthly.getFinancialDocumentMonth12LineAmount().isNonZero();
408 if (nonZeroMonthlyExists) {
409 isValid &= false;
410 errors.putError(BCPropertyConstants.FINANCIAL_DOCUMENT_MONTH1_LINE_AMOUNT, BCKeyConstants.ERROR_MONTHLY_TOTAL_ZERO);
411 }
412 }
413 }
414 else {
415 LOG.info("business rule checks failed in processSaveMonthlyBudgetRules in BudgetConstructionDocumentRules");
416 }
417
418 LOG.debug("processSaveMonthlyBudgetRules() end");
419 return isValid;
420 }
421
422 /**
423 * Iterates existing revenue or expenditure lines. Checks if request amount is non-zero or has changed and runs business rules
424 * on the line.
425 *
426 * @param budgetConstructionDocument
427 * @param errors
428 * @param isRevenue
429 * @return
430 */
431 protected boolean checkPendingBudgetConstructionGeneralLedgerLines(BudgetConstructionDocument budgetConstructionDocument, MessageMap errors, boolean isRevenue, boolean doMonthRICheck) {
432
433 boolean isValid = true;
434 boolean isReqAmountValid;
435 int originalErrorCount;
436 int currentErrorCount;
437 List<PendingBudgetConstructionGeneralLedger> pendingBudgetConstructionGeneralLedgerLines;
438 String linesErrorPath;
439
440
441 if (isRevenue) {
442 pendingBudgetConstructionGeneralLedgerLines = budgetConstructionDocument.getPendingBudgetConstructionGeneralLedgerRevenueLines();
443 linesErrorPath = BCPropertyConstants.PENDING_BUDGET_CONSTRUCTION_GENERAL_LEDGER_REVENUE_LINES;
444 }
445 else {
446 pendingBudgetConstructionGeneralLedgerLines = budgetConstructionDocument.getPendingBudgetConstructionGeneralLedgerExpenditureLines();
447 linesErrorPath = BCPropertyConstants.PENDING_BUDGET_CONSTRUCTION_GENERAL_LEDGER_EXPENDITURE_LINES;
448 }
449
450 // iterate revenue or expenditure lines
451 Integer index = 0;
452 for (Iterator iter = pendingBudgetConstructionGeneralLedgerLines.iterator(); iter.hasNext(); index++) {
453
454 PendingBudgetConstructionGeneralLedger element = (PendingBudgetConstructionGeneralLedger) iter.next();
455 errors.addToErrorPath(linesErrorPath + "[" + index + "]");
456
457 originalErrorCount = errors.getErrorCount();
458
459 // run dd required field and format checks on request amount only, since only it can be changed by user
460 // no sanity checks on hiddens and readonly field params
461 validatePrimitiveFromDescriptor(element, TARGET_ERROR_PROPERTY_NAME, "", true);
462
463 // check to see if any errors were reported
464 currentErrorCount = errors.getErrorCount();
465 isReqAmountValid = (currentErrorCount == originalErrorCount);
466 isValid &= isReqAmountValid;
467
468 // test for new errors from this point - if none, test if benefits calc required
469 originalErrorCount = errors.getErrorCount();
470
471 // has the request amount changed?
472 boolean isRequestAmountChanged = (isReqAmountValid && (!element.getAccountLineAnnualBalanceAmount().equals(element.getPersistedAccountLineAnnualBalanceAmount())));
473
474 // only do checks if request amount is non-zero and not equal to currently persisted amount
475 // or the document is not budgetable and the request is non-zero
476 if (isReqAmountValid && element.getAccountLineAnnualBalanceAmount().isNonZero()) {
477
478 boolean isSalaryFringeLine = false;
479 if (!isRevenue && fringeBenefitDesignatorCodesParamValues != null && element.getLaborObject() != null) {
480 isSalaryFringeLine = fringeBenefitDesignatorCodesParamValues.contains(element.getLaborObject().getFinancialObjectFringeOrSalaryCode());
481 }
482 boolean is2PLG = !isRevenue && element.getFinancialObjectCode().contentEquals(KFSConstants.BudgetConstructionConstants.OBJECT_CODE_2PLG);
483 boolean isCleanupModeActionForceCheck = budgetConstructionDocument.isCleanupModeActionForceCheck();
484
485 // Request notZero, do checks if user enters a change to a request amount or
486 // (We are in cleanupMode and the current action (save or close-save) forces a cleanup mode check and
487 // not 2PLG line and not salary fringe line)
488 // This allows the user to use quick salary setting, monthly edit, global month delete to do cleanup work and
489 // to print out values or push/pull before cleanup.
490 if (isRequestAmountChanged || (!budgetConstructionDocument.isBudgetableDocument() && isCleanupModeActionForceCheck && !is2PLG && !isSalaryFringeLine)) {
491 isValid &= this.checkPendingBudgetConstructionGeneralLedgerLine(budgetConstructionDocument, element, errors, isRevenue, false);
492 }
493 }
494
495 // Do RI type checks for request amount against monthly and salary setting detail if persisted amount changes
496 // or a 2plg exists and the line is a salary setting detail line
497 // Also tests if the line is has benefits associate and flags that a benefits calculation needs done.
498 // Benefits calc is then called in the form action after successful rules check and save
499 boolean forceTwoPlugRICheck = (budgetConstructionDocument.isContainsTwoPlug() && (element.getLaborObject() != null && element.getLaborObject().isDetailPositionRequiredIndicator()));
500
501 // force monthly RI check if 2PLG and if request amount changes AND not a detail salary setting line
502 boolean forceMonthlyRICheck = (budgetConstructionDocument.isContainsTwoPlug() && (element.getLaborObject() == null || !element.getLaborObject().isDetailPositionRequiredIndicator()));
503
504 if (isReqAmountValid && (isRequestAmountChanged || forceTwoPlugRICheck)) {
505
506 // check monthly for all rows
507 if (doMonthRICheck || forceMonthlyRICheck) {
508 if (element.getBudgetConstructionMonthly() != null && !element.getBudgetConstructionMonthly().isEmpty()) {
509
510 BudgetConstructionMonthly budgetConstructionMonthly = element.getBudgetConstructionMonthly().get(0);
511 if (budgetConstructionMonthly != null) {
512 KualiInteger monthSum = KualiInteger.ZERO;
513 monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth1LineAmount());
514 monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth2LineAmount());
515 monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth3LineAmount());
516 monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth4LineAmount());
517 monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth5LineAmount());
518 monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth6LineAmount());
519 monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth7LineAmount());
520 monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth8LineAmount());
521 monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth9LineAmount());
522 monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth10LineAmount());
523 monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth11LineAmount());
524 monthSum = monthSum.add(budgetConstructionMonthly.getFinancialDocumentMonth12LineAmount());
525
526 if (!monthSum.equals(element.getAccountLineAnnualBalanceAmount())) {
527 isValid &= false;
528 String pkeyVal = element.getFinancialObjectCode() + "," + element.getFinancialSubObjectCode();
529 GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT, BCKeyConstants.ERROR_MONTHLY_SUM_REQUEST_NOT_EQUAL, pkeyVal, monthSum.toString(), element.getAccountLineAnnualBalanceAmount().toString());
530 }
531 }
532 }
533 }
534
535 // check salary setting detail sum if expenditure line is a ss detail line
536 // and salary setting option is turned on
537 if (!SpringContext.getBean(SalarySettingService.class).isSalarySettingDisabled()) {
538 if (element.getLaborObject() != null) {
539 if (element.getLaborObject().isDetailPositionRequiredIndicator()) {
540
541 // sum the detail lines and compare against the accounting line request amount
542 KualiInteger salarySum = KualiInteger.ZERO;
543
544 // if salary setting detail exists, sum it otherwise default to zero
545 if (element.isPendingBudgetConstructionAppointmentFundingExists()) {
546
547 // run reportquery to get the salary request sum
548 salarySum = SpringContext.getBean(BudgetDocumentService.class).getPendingBudgetConstructionAppointmentFundingRequestSum(element);
549
550 }
551
552 if (!salarySum.equals(element.getAccountLineAnnualBalanceAmount())) {
553 isValid &= false;
554 String pkeyVal = element.getFinancialObjectCode() + "," + element.getFinancialSubObjectCode();
555 GlobalVariables.getMessageMap().putError(KFSPropertyConstants.ACCOUNT_LINE_ANNUAL_BALANCE_AMOUNT, BCKeyConstants.ERROR_SALARY_SUM_REQUEST_NOT_EQUAL, pkeyVal, salarySum.toString(), element.getAccountLineAnnualBalanceAmount().toString());
556 }
557 }
558 }
559 }
560
561 // only do benefits calc needed test if the user changed something - not if forcing the RI check
562 if (isReqAmountValid && !element.getAccountLineAnnualBalanceAmount().equals(element.getPersistedAccountLineAnnualBalanceAmount())) {
563
564 // if benefits calculation is turned on,
565 // check if the line is benefits related and call for calculation after save
566 if (!SpringContext.getBean(BenefitsCalculationService.class).isBenefitsCalculationDisabled()) {
567
568 // retest for added errors since testing this line started - if none, test if benefits calc required
569 currentErrorCount = errors.getErrorCount();
570 isReqAmountValid = (currentErrorCount == originalErrorCount);
571
572 if (isReqAmountValid && element.getPositionObjectBenefit() != null && !element.getPositionObjectBenefit().isEmpty()) {
573 budgetConstructionDocument.setBenefitsCalcNeeded(true);
574 }
575 }
576 }
577 }
578
579 errors.removeFromErrorPath(linesErrorPath + "[" + index + "]");
580 }
581
582 return isValid;
583 }
584
585 /**
586 * Checks a PBGL line. Assumes the line has been checked against the dd for formatting and if required
587 *
588 * @param budgetConstructionDocument
589 * @param pendingBudgetConstructionGeneralLedger
590 * @return
591 */
592 protected boolean checkPendingBudgetConstructionGeneralLedgerLine(BudgetConstructionDocument budgetConstructionDocument, PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger, MessageMap errors, boolean isRevenue, boolean isAdd) {
593 LOG.debug("checkPendingBudgetConstructionGeneralLedgerLine() start");
594
595 boolean isValid = true;
596
597 // now make sure all the necessary business objects are fully populated
598 // this refreshes any refs not done by populate for display purposes auto-update="none"
599 pendingBudgetConstructionGeneralLedger.refreshNonUpdateableReferences();
600
601 isValid &= validatePBGLLine(pendingBudgetConstructionGeneralLedger, isAdd);
602 if (isValid) {
603
604 // all lines must have objects defined with financialBudgetAggregation = 'O';
605 isValid &= isBudgetAggregationAllowed(budgetAggregationCodesParamValues, pendingBudgetConstructionGeneralLedger, errors, isAdd);
606
607 isValid &= this.isBudgetAllowed(budgetConstructionDocument, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, errors, isAdd, false);
608
609 // revenue specific checks
610 if (isRevenue) {
611
612 // no revenue lines in CnG accounts or SDCI
613 isValid &= isNotSalarySettingOnly(salarySettingFundGroupsParamValues, salarySettingSubFundGroupsParamValues, budgetConstructionDocument, pendingBudgetConstructionGeneralLedger, errors, isRevenue, isAdd);
614
615 // line must use matching revenue object type
616 isValid &= isObjectTypeAllowed(revenueObjectTypesParamValues, pendingBudgetConstructionGeneralLedger, errors, isRevenue, isAdd);
617
618 }
619 else {
620 // expenditure specific checks
621
622 // line must use matching expenditure object type
623 isValid &= isObjectTypeAllowed(expenditureObjectTypesParamValues, pendingBudgetConstructionGeneralLedger, errors, isRevenue, isAdd);
624
625 // no lines using labor objects in non-wage accounts
626 isValid &= isNonWagesAccountNotLaborObject(budgetConstructionDocument, pendingBudgetConstructionGeneralLedger, errors, isAdd);
627
628 // only lines using detail labor objects allowed in fund group CG and sfund group SDCI
629 isValid &= isNotSalarySettingOnly(salarySettingFundGroupsParamValues, salarySettingSubFundGroupsParamValues, budgetConstructionDocument, pendingBudgetConstructionGeneralLedger, errors, isRevenue, isAdd);
630
631 // no lines using fringe benefit target object codes allowed to be manually added by user
632 // the lines are created by benefits calculation process
633 isValid &= isNotFringeBenefitObject(fringeBenefitDesignatorCodesParamValues, pendingBudgetConstructionGeneralLedger, errors, isAdd);
634 }
635
636 }
637
638 if (!isValid) {
639 LOG.info("business rule checks failed in checkPendingBudgetConstructionGeneralLedgerLine in BudgetConstructionRules");
640 }
641
642 LOG.debug("checkPendingBudgetConstructionGeneralLedgerLine() end");
643 return isValid;
644 }
645
646 protected boolean validatePBGLLine(PendingBudgetConstructionGeneralLedger pendingBudgetConstructionGeneralLedger, boolean isAdd) {
647 if (pendingBudgetConstructionGeneralLedger == null) {
648 throw new IllegalStateException(getKualiConfigurationService().getPropertyString(KFSKeyConstants.ERROR_DOCUMENT_NULL_ACCOUNTING_LINE));
649 }
650
651 // grab the service instance that will be needed by all the validate methods
652 DataDictionary dd = dataDictionaryService.getDataDictionary();
653
654 // retrieve each pbgl line object and validate
655 boolean valid = true;
656
657 // object code is required
658 ObjectCode objectCode = pendingBudgetConstructionGeneralLedger.getFinancialObject();
659
660 // this code calls a local version (not AccountingLineRuleHelper) of isValidObjectCode to add the bad value to the error
661 // message
662 if (isAdd) {
663 valid &= isValidObjectCode(objectCode, pendingBudgetConstructionGeneralLedger.getFinancialObjectCode(), dd, KFSConstants.FINANCIAL_OBJECT_CODE_PROPERTY_NAME);
664 }
665 else {
666 valid &= isValidObjectCode(objectCode, pendingBudgetConstructionGeneralLedger.getFinancialObjectCode(), dd, TARGET_ERROR_PROPERTY_NAME);
667 }
668
669 // sub object is not required
670 if (StringUtils.isNotBlank(pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode()) && !pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode().equalsIgnoreCase(KFSConstants.getDashFinancialSubObjectCode())) {
671 SubObjectCode subObjectCode = pendingBudgetConstructionGeneralLedger.getFinancialSubObject();
672
673 // this code calls a local version (not AccountingLineRuleHelper) of isValidSubObjectCode to add the bad value to the
674 // error message
675 if (isAdd) {
676 valid &= isValidSubObjectCode(subObjectCode, pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode(), dd, KFSConstants.FINANCIAL_SUB_OBJECT_CODE_PROPERTY_NAME);
677 }
678 else {
679 valid &= isValidSubObjectCode(subObjectCode, pendingBudgetConstructionGeneralLedger.getFinancialSubObjectCode(), dd, TARGET_ERROR_PROPERTY_NAME);
680 }
681 }
682
683 return valid;
684 }
685
686 /**
687 * Validates a single primitive in a BO
688 *
689 * @param object
690 * @param attributeName
691 * @param errorPrefix
692 * @param validateRequired
693 */
694 protected void validatePrimitiveFromDescriptor(Object object, String attributeName, String errorPrefix, boolean validateRequired) {
695
696 try {
697 PropertyDescriptor attributeDescriptor = PropertyUtils.getPropertyDescriptor(object, attributeName);
698 validatePrimitiveFromDescriptor(object.getClass().getName(), object, attributeDescriptor, "", true);
699 }
700 catch (NoSuchMethodException e) {
701 throw new InfrastructureException("unable to find propertyDescriptor for property '" + attributeName + "'", e);
702 }
703 catch (IllegalAccessException e) {
704 throw new InfrastructureException("unable to access propertyDescriptor for property '" + attributeName + "'", e);
705 }
706 catch (InvocationTargetException e) {
707 throw new InfrastructureException("unable to invoke methods for property '" + attributeName + "'", e);
708 }
709 }
710
711 /**
712 * Validates a primitive in a BO
713 *
714 * @param entryName
715 * @param object
716 * @param propertyDescriptor
717 * @param errorPrefix
718 * @param validateRequired
719 */
720 protected void validatePrimitiveFromDescriptor(String entryName, Object object, PropertyDescriptor propertyDescriptor, String errorPrefix, boolean validateRequired) {
721
722 // validate the primitive attributes if defined in the dictionary
723 if (null != propertyDescriptor && dataDictionaryService.isAttributeDefined(entryName, propertyDescriptor.getName())) {
724 Object value = ObjectUtils.getPropertyValue(object, propertyDescriptor.getName());
725 Class propertyType = propertyDescriptor.getPropertyType();
726
727 if (TypeUtils.isStringClass(propertyType) || TypeUtils.isIntegralClass(propertyType) || TypeUtils.isDecimalClass(propertyType) || TypeUtils.isTemporalClass(propertyType)) {
728
729 // check value format against dictionary
730 if (value != null && StringUtils.isNotBlank(value.toString())) {
731 if (!TypeUtils.isTemporalClass(propertyType)) {
732 getDictionaryValidationService().validateAttributeFormat(entryName, propertyDescriptor.getName(), value.toString(), errorPrefix + propertyDescriptor.getName());
733 }
734 }
735 else if (validateRequired) {
736 getDictionaryValidationService().validateAttributeRequired(entryName, propertyDescriptor.getName(), value, Boolean.FALSE, errorPrefix + propertyDescriptor.getName());
737 }
738 }
739 }
740 }
741
742 protected boolean isObjectTypeAllowed(List paramValues, PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isRevenue, boolean isAdd) {
743 boolean isAllowed = true;
744
745 if (paramValues != null) {
746 if (!paramValues.contains(accountingLine.getFinancialObject().getFinancialObjectTypeCode())) {
747 isAllowed = false;
748
749 String targetErrorProperty;
750 if (isAdd) {
751 targetErrorProperty = KFSPropertyConstants.FINANCIAL_OBJECT_CODE;
752 }
753 else {
754 targetErrorProperty = TARGET_ERROR_PROPERTY_NAME;
755 }
756
757 if (isRevenue) {
758 this.putError(errors, targetErrorProperty, KFSKeyConstants.ERROR_DOCUMENT_EXPENSE_ON_INCOME_SIDE, isAdd, accountingLine.getFinancialObjectCode());
759 }
760 else {
761 this.putError(errors, targetErrorProperty, KFSKeyConstants.ERROR_DOCUMENT_INCOME_ON_EXPENSE_SIDE, isAdd, accountingLine.getFinancialObjectCode());
762 }
763 }
764 }
765 else {
766 isAllowed = false;
767 }
768
769 return isAllowed;
770 }
771
772 protected boolean isBudgetAggregationAllowed(List paramValues, PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isAdd) {
773 boolean isAllowed = true;
774
775 if (paramValues != null) {
776 if (!paramValues.contains(accountingLine.getFinancialObject().getFinancialBudgetAggregationCd())) {
777 isAllowed = false;
778
779 this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, KFSKeyConstants.ERROR_DOCUMENT_INCORRECT_OBJ_CODE_WITH_BUDGET_AGGREGATION, isAdd, accountingLine.getFinancialObjectCode(), accountingLine.getFinancialObject().getFinancialBudgetAggregationCd());
780 }
781 }
782 else {
783 isAllowed = false;
784 }
785
786 return isAllowed;
787 }
788
789 protected boolean isNewLineUnique(BudgetConstructionDocument budgetConstructionDocument, PendingBudgetConstructionGeneralLedger newLine, MessageMap errors, boolean isRevenue) {
790 boolean isUnique = true;
791 List<PendingBudgetConstructionGeneralLedger> existingLines;
792
793 if (isRevenue) {
794 existingLines = budgetConstructionDocument.getPendingBudgetConstructionGeneralLedgerRevenueLines();
795 }
796 else {
797 existingLines = budgetConstructionDocument.getPendingBudgetConstructionGeneralLedgerExpenditureLines();
798 }
799
800 if (BudgetConstructionRuleUtil.hasExistingPBGLLine(existingLines, newLine)) {
801 isUnique = false;
802 errors.putError(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_BUDGET_LINE_EXISTS, newLine.getFinancialObjectCode() + "," + newLine.getFinancialSubObjectCode());
803 }
804
805 return isUnique;
806 }
807
808 protected boolean isNonWagesAccountNotLaborObject(BudgetConstructionDocument budgetConstructionDocument, PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isAdd) {
809 boolean isAllowed = true;
810
811 if (budgetConstructionDocument.getAccount().getSubFundGroup() == null || !budgetConstructionDocument.getAccount().getSubFundGroup().isSubFundGroupWagesIndicator()) {
812 if (accountingLine.getLaborObject() != null) {
813 isAllowed = false;
814 this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_LABOR_OBJECT_IN_NOWAGES_ACCOUNT, isAdd, accountingLine.getFinancialObjectCode());
815 }
816 }
817 return isAllowed;
818 }
819
820 protected boolean isNotFringeBenefitObject(List paramValues, PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isAdd) {
821 boolean isAllowed = true;
822
823 if (paramValues != null) {
824 if (accountingLine.getLaborObject() != null) {
825 if (paramValues.contains(accountingLine.getLaborObject().getFinancialObjectFringeOrSalaryCode())) {
826 isAllowed = false;
827 this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_FRINGE_BENEFIT_OBJECT_NOT_ALLOWED, isAdd, accountingLine.getFinancialObjectCode());
828 }
829 }
830 }
831 else {
832 isAllowed = false;
833 }
834
835 return isAllowed;
836 }
837
838 protected boolean isNotSalarySettingOnly(List fundGroupParamValues, List subfundGroupParamValues, BudgetConstructionDocument budgetConstructionDocument, PendingBudgetConstructionGeneralLedger accountingLine, MessageMap errors, boolean isRevenue, boolean isAdd) {
839 boolean isAllowed = true;
840
841 // check if account belongs to a fund or subfund that only allows salary setting lines
842 AccountSalarySettingOnlyCause retVal = budgetParameterService.isSalarySettingOnlyAccount(budgetConstructionDocument);
843 if (retVal != AccountSalarySettingOnlyCause.MISSING_PARAM) {
844 if (retVal != AccountSalarySettingOnlyCause.NONE) {
845
846 // the line must use an object that is a detail salary labor object
847 if (isRevenue || accountingLine.getLaborObject() == null || !accountingLine.getLaborObject().isDetailPositionRequiredIndicator()) {
848
849 isAllowed = false;
850 if (retVal == AccountSalarySettingOnlyCause.FUND || retVal == AccountSalarySettingOnlyCause.FUND_AND_SUBFUND) {
851 this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_SALARY_SETTING_OBJECT_ONLY, isAdd, "fund " + budgetConstructionDocument.getAccount().getSubFundGroup().getFundGroupCode());
852
853 }
854 if (retVal == AccountSalarySettingOnlyCause.SUBFUND || retVal == AccountSalarySettingOnlyCause.FUND_AND_SUBFUND) {
855 this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_SALARY_SETTING_OBJECT_ONLY, isAdd, "subfund " + budgetConstructionDocument.getAccount().getSubFundGroup().getSubFundGroupCode());
856 }
857 }
858 }
859
860 }
861 else {
862 // missing system parameter
863 this.putError(errors, KFSPropertyConstants.FINANCIAL_OBJECT_CODE, BCKeyConstants.ERROR_SALARY_SETTING_OBJECT_ONLY_NO_PARAMETER, isAdd, budgetConstructionDocument.getAccount().getSubFundGroup().getFundGroupCode() + "," + budgetConstructionDocument.getAccount().getSubFundGroup().getSubFundGroupCode());
864 isAllowed = false;
865 }
866
867 return isAllowed;
868 }
869
870 /**
871 * runs rule checks that don't allow a budget
872 *
873 * @param budgetConstructionDocument
874 * @param propertyName
875 * @param errors
876 * @param isAdd
877 * @param isDocumentAdd
878 * @return
879 */
880 protected boolean isBudgetAllowed(BudgetConstructionDocument budgetConstructionDocument, String propertyName, MessageMap errors, boolean isAdd, boolean isDocumentAdd) {
881 boolean isAllowed = true;
882 SimpleDateFormat tdf = new SimpleDateFormat("MM/dd/yyyy hh:mm a");
883
884 // is account closed?
885 if (!budgetConstructionDocument.getAccount().isActive()) {
886 isAllowed = false;
887 this.putError(errors, propertyName, KFSKeyConstants.ERROR_CLOSED, isAdd, "account: " + budgetConstructionDocument.getAccountNumber());
888 }
889
890 // is account expiration no budget allowed, currently < 1/1/(byfy-2)?
891 Calendar expDate = BudgetConstructionRuleUtil.getNoBudgetAllowedExpireDate(budgetConstructionDocument.getUniversityFiscalYear());
892 if (budgetConstructionDocument.getAccount().isExpired(expDate)) {
893 isAllowed = false;
894 this.putError(errors, propertyName, BCKeyConstants.ERROR_NO_BUDGET_ALLOWED, isAdd, budgetConstructionDocument.getAccountNumber(), tdf.format(budgetConstructionDocument.getAccount().getAccountExpirationDate()));
895
896 }
897
898 // is account a cash control account
899 if (budgetConstructionDocument.getAccount().getBudgetRecordingLevelCode().equalsIgnoreCase(BCConstants.BUDGET_RECORDING_LEVEL_N)) {
900 isAllowed = false;
901 this.putError(errors, propertyName, BCKeyConstants.ERROR_BUDGET_RECORDING_LEVEL_NOT_ALLOWED, isAdd, budgetConstructionDocument.getAccountNumber(), BCConstants.BUDGET_RECORDING_LEVEL_N);
902 }
903
904 // grab the service instance that will be needed by all the validate methods
905 DataDictionary dd = dataDictionaryService.getDataDictionary();
906
907 if (StringUtils.isNotBlank(budgetConstructionDocument.getSubAccountNumber()) && !budgetConstructionDocument.getSubAccountNumber().equalsIgnoreCase(KFSConstants.getDashSubAccountNumber())) {
908 SubAccount subAccount = budgetConstructionDocument.getSubAccount();
909
910 // is subacct inactive or not exist?
911 // this code calls a local version (not AccountingLineRuleHelper) of isValidSubAccount
912 // to add the bad value to the error message
913 if (isAdd) {
914 if (isDocumentAdd) {
915 isAllowed &= this.isValidSubAccount(subAccount, budgetConstructionDocument.getSubAccountNumber(), dd, KFSPropertyConstants.SUB_ACCOUNT_NUMBER);
916 }
917 else {
918 isAllowed &= this.isValidSubAccount(subAccount, budgetConstructionDocument.getSubAccountNumber(), dd, propertyName);
919 }
920 }
921 else {
922 isAllowed &= this.isValidSubAccount(subAccount, budgetConstructionDocument.getSubAccountNumber(), dd, TARGET_ERROR_PROPERTY_NAME);
923 }
924
925 // is subacct type cost share?
926 // this hack is here since kuldev is missing one to one instances
927 // and the RI ojb mapping produces an error when attempting to test if the
928 // A21SubAccount attached to the document's SubAccount is null
929 Map<String, Object> searchCriteria = new HashMap<String, Object>();
930 searchCriteria.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, budgetConstructionDocument.getChartOfAccountsCode());
931 searchCriteria.put(KFSPropertyConstants.ACCOUNT_NUMBER, budgetConstructionDocument.getAccountNumber());
932 searchCriteria.put(KFSPropertyConstants.SUB_ACCOUNT_NUMBER, budgetConstructionDocument.getSubAccountNumber());
933 A21SubAccount a21SubAccount = (A21SubAccount) businessObjectService.findByPrimaryKey(A21SubAccount.class, searchCriteria);
934 if (ObjectUtils.isNotNull(a21SubAccount)) {
935 if (a21SubAccount.getSubAccountTypeCode().equalsIgnoreCase(KFSConstants.SubAccountType.COST_SHARE)) {
936 isAllowed = false;
937 this.putError(errors, propertyName, BCKeyConstants.ERROR_SUB_ACCOUNT_TYPE_NOT_ALLOWED, isAdd, budgetConstructionDocument.getAccountNumber(), KFSConstants.SubAccountType.COST_SHARE);
938 }
939 }
940 }
941
942 return isAllowed;
943 }
944
945 public boolean isValidAccount(Account account, String value, DataDictionary dataDictionary, String errorPropertyName) {
946 String label = dataDictionary.getBusinessObjectEntry(Account.class.getName()).getAttributeDefinition(KFSConstants.ACCOUNT_NUMBER_PROPERTY_NAME).getShortLabel();
947
948 // make sure it exists
949 if (ObjectUtils.isNull(account)) {
950 GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_EXISTENCE, label + ":" + value);
951 return false;
952 }
953
954 return true;
955 }
956
957 public boolean isValidSubAccount(SubAccount subAccount, String value, DataDictionary dataDictionary, String errorPropertyName) {
958 String label = dataDictionary.getBusinessObjectEntry(SubAccount.class.getName()).getAttributeDefinition(KFSConstants.SUB_ACCOUNT_NUMBER_PROPERTY_NAME).getShortLabel();
959
960 // make sure it exists
961 if (ObjectUtils.isNull(subAccount)) {
962 GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_EXISTENCE, label + ":" + value);
963 return false;
964 }
965
966 // check to make sure it is active
967 if (!subAccount.isActive()) {
968 GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_DOCUMENT_SUB_ACCOUNT_INACTIVE, label + ":" + value);
969 return false;
970 }
971
972 return true;
973 }
974
975 /**
976 * Runs existence and active tests on the SubObjectCode reference This method is differenct than the one in
977 * AccountingLineRuleHelper in that it adds the bad value to the errormessage This method signature should probably be added to
978 * AccountingLineRuleHelper
979 *
980 * @param subObjectCode
981 * @param value
982 * @param dataDictionary
983 * @param errorPropertyName
984 * @return
985 */
986 public boolean isValidSubObjectCode(SubObjectCode subObjectCode, String value, DataDictionary dataDictionary, String errorPropertyName) {
987 String label = dataDictionary.getBusinessObjectEntry(SubObjectCode.class.getName()).getAttributeDefinition(KFSConstants.FINANCIAL_SUB_OBJECT_CODE_PROPERTY_NAME).getShortLabel();
988
989 // make sure it exists
990 if (ObjectUtils.isNull(subObjectCode)) {
991 GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_EXISTENCE, label + ":" + value);
992 return false;
993 }
994
995 // check active flag
996 if (!subObjectCode.isActive()) {
997 GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_INACTIVE, label + ":" + value);
998 return false;
999 }
1000 return true;
1001 }
1002
1003 /**
1004 * Runs existence and active tests on the ObjectCode reference This method is differenct than the one in
1005 * AccountingLineRuleHelper in that it adds the bad value to the errormessage This method signature should probably be added to
1006 * AccountingLineRuleHelper
1007 *
1008 * @param objectCode
1009 * @param value
1010 * @param dataDictionary
1011 * @param errorPropertyName
1012 * @return
1013 */
1014 public boolean isValidObjectCode(ObjectCode objectCode, String value, DataDictionary dataDictionary, String errorPropertyName) {
1015 String label = dataDictionary.getBusinessObjectEntry(ObjectCode.class.getName()).getAttributeDefinition(KFSConstants.FINANCIAL_OBJECT_CODE_PROPERTY_NAME).getShortLabel();
1016
1017 // make sure it exists
1018 if (ObjectUtils.isNull(objectCode)) {
1019 GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_EXISTENCE, label + ":" + value);
1020 return false;
1021 }
1022
1023 // check active status
1024 if (!objectCode.isFinancialObjectActiveCode()) {
1025 GlobalVariables.getMessageMap().putError(errorPropertyName, KFSKeyConstants.ERROR_INACTIVE, label + ":" + value);
1026 return false;
1027 }
1028
1029 return true;
1030 }
1031
1032 /**
1033 * puts error to errormap for propertyName if isAdd, otherwise the property name is replaced with value of
1034 * TARGET_ERROR_PROPERTY_NAME
1035 *
1036 * @param propertyName
1037 * @param errorKey
1038 * @param isAdd
1039 * @param errorParameters
1040 */
1041 protected void putError(MessageMap errors, String propertyName, String errorKey, boolean isAdd, String... errorParameters) {
1042
1043 if (isAdd) {
1044 errors.putError(propertyName, errorKey, errorParameters);
1045 }
1046 else {
1047 errors.putError(TARGET_ERROR_PROPERTY_NAME, errorKey, errorParameters);
1048 }
1049
1050 }
1051 }