001: /*
002: * Copyright 2007 The Kuali Foundation.
003: *
004: * Licensed under the Educational Community License, Version 1.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/ecl1.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.rules;
017:
018: import static org.kuali.kfs.KFSConstants.BALANCE_TYPE_ACTUAL;
019: import static org.kuali.kfs.KFSConstants.BLANK_SPACE;
020: import static org.kuali.kfs.KFSConstants.GL_CREDIT_CODE;
021: import static org.kuali.kfs.KFSConstants.GL_DEBIT_CODE;
022:
023: import java.sql.Timestamp;
024: import java.text.MessageFormat;
025: import java.util.List;
026:
027: import org.apache.commons.lang.StringUtils;
028: import org.kuali.core.datadictionary.AttributeDefinition;
029: import org.kuali.core.datadictionary.DataDictionary;
030: import org.kuali.core.datadictionary.DataDictionaryEntry;
031: import org.kuali.core.service.DataDictionaryService;
032: import org.kuali.core.service.DateTimeService;
033: import org.kuali.core.service.DocumentTypeService;
034: import org.kuali.core.service.KualiConfigurationService;
035: import org.kuali.core.util.GeneralLedgerPendingEntrySequenceHelper;
036: import org.kuali.core.util.GlobalVariables;
037: import org.kuali.core.util.KualiDecimal;
038: import org.kuali.core.util.ObjectUtils;
039: import org.kuali.kfs.KFSConstants;
040: import org.kuali.kfs.KFSKeyConstants;
041: import org.kuali.kfs.bo.AccountingLine;
042: import org.kuali.kfs.bo.GeneralLedgerPendingEntry;
043: import org.kuali.kfs.context.SpringContext;
044: import org.kuali.kfs.document.AccountingDocument;
045: import org.kuali.kfs.service.HomeOriginationService;
046: import org.kuali.module.chart.bo.Account;
047: import org.kuali.module.chart.bo.AccountingPeriod;
048: import org.kuali.module.chart.bo.ObjectCode;
049: import org.kuali.module.chart.bo.SubAccount;
050: import org.kuali.module.chart.bo.SubObjCd;
051: import org.kuali.module.chart.bo.codes.BalanceTyp;
052: import org.kuali.module.chart.service.ObjectTypeService;
053: import org.kuali.module.financial.bo.BankAccount;
054: import org.kuali.module.gl.service.SufficientFundsService;
055:
056: /**
057: * This utility class contains some common transactional document business rules, that can be used by many different parts of this
058: * application.
059: */
060: public class AccountingDocumentRuleUtil {
061: /**
062: * Helper method for accessing the <code>{@link DataDictionary}</code>
063: *
064: * @return DataDictionary
065: */
066: private static DataDictionary getDataDictionary() {
067: return SpringContext.getBean(DataDictionaryService.class)
068: .getDataDictionary();
069: }
070:
071: /**
072: * This method checks for the existence of the provided balance type, in the system and also checks to see if it is active.
073: *
074: * @param balanceType
075: * @param errorPropertyName also used as the BalanceTyp DD attribute name
076: * @return True if the balance type is valid, false otherwise.
077: */
078: public static boolean isValidBalanceType(BalanceTyp balanceType,
079: String errorPropertyName) {
080: return isValidBalanceType(balanceType, BalanceTyp.class,
081: errorPropertyName, errorPropertyName);
082: }
083:
084: /**
085: * This method checks for the existence of the provided balance type, in the system and also checks to see if it is active.
086: *
087: * @param balanceType
088: * @param entryClass the Class of the DataDictionary entry containing the attribute with the label for the error message
089: * @param attributeName the name of the attribute in the DataDictionary entry
090: * @param errorPropertyName
091: * @return True if the balance type is valid, false otherwise.
092: */
093: public static boolean isValidBalanceType(BalanceTyp balanceType,
094: Class entryClass, String attributeName,
095: String errorPropertyName) {
096: String label = getLabelFromDataDictionary(entryClass,
097: attributeName);
098: if (ObjectUtils.isNull(balanceType)) {
099: GlobalVariables.getErrorMap().putError(errorPropertyName,
100: KFSKeyConstants.ERROR_EXISTENCE, label);
101: return false;
102: }
103: // make sure it's active for usage
104: if (!balanceType.isActive()) {
105: GlobalVariables.getErrorMap().putError(errorPropertyName,
106: KFSKeyConstants.ERROR_INACTIVE, label);
107: return false;
108: }
109: return true;
110: }
111:
112: private static String getLabelFromDataDictionary(Class entryClass,
113: String attributeName) {
114: DataDictionaryEntry entry = getDataDictionary()
115: .getDictionaryObjectEntry(entryClass.getName());
116: if (entry == null) {
117: throw new IllegalArgumentException(
118: "Cannot find DataDictionary entry for "
119: + entryClass);
120: }
121: AttributeDefinition attributeDefinition = entry
122: .getAttributeDefinition(attributeName);
123: if (attributeDefinition == null) {
124: throw new IllegalArgumentException("Cannot find "
125: + entryClass + " attribute with name "
126: + attributeName);
127: }
128: return attributeDefinition.getLabel();
129: }
130:
131: /**
132: * This method checks for the existence of the accounting period in the system and also makes sure that the accounting period is
133: * open for posting.
134: *
135: * @param accountingPeriod
136: * @param entryClass
137: * @param attribueName
138: * @param errorPropertyName
139: * @return True if the accounting period exists in the system and is open for posting, false otherwise.
140: */
141: public static boolean isValidOpenAccountingPeriod(
142: AccountingPeriod accountingPeriod, Class entryClass,
143: String attribueName, String errorPropertyName) {
144: // retrieve from system to make sure it exists
145: String label = getLabelFromDataDictionary(entryClass,
146: attribueName);
147: if (ObjectUtils.isNull(accountingPeriod)) {
148: GlobalVariables.getErrorMap().putError(errorPropertyName,
149: KFSKeyConstants.ERROR_EXISTENCE, label);
150: return false;
151: }
152:
153: // make sure it's open for use
154: if (accountingPeriod.getUniversityFiscalPeriodStatusCode()
155: .equals(KFSConstants.ACCOUNTING_PERIOD_STATUS_CLOSED)) {
156: GlobalVariables
157: .getErrorMap()
158: .putError(
159: errorPropertyName,
160: KFSKeyConstants.ERROR_DOCUMENT_ACCOUNTING_PERIOD_CLOSED);
161: return false;
162: }
163:
164: return true;
165: }
166:
167: /**
168: * Some documents have reversal dates. This method represents the common implementation that transactional documents follow for
169: * reversal dates - that they must not be before the current date.
170: *
171: * @param reversalDate
172: * @param errorPropertyName
173: * @return boolean True if the reversal date is not earlier than the current date, false otherwise.
174: */
175: public static boolean isValidReversalDate(
176: java.sql.Date reversalDate, String errorPropertyName) {
177: java.sql.Date today = SpringContext.getBean(
178: DateTimeService.class).getCurrentSqlDateMidnight();
179: if (null != reversalDate && reversalDate.before(today)) {
180: GlobalVariables
181: .getErrorMap()
182: .putError(
183: errorPropertyName,
184: KFSKeyConstants.ERROR_DOCUMENT_INCORRECT_REVERSAL_DATE);
185: return false;
186: } else {
187: return true;
188: }
189: }
190:
191: /**
192: * Determines whether an accounting line is an income line or not. This goes agains the configurable object type code list in
193: * the ApplicationParameter mechanism. This list can be configured externally.
194: *
195: * @param accountingLine
196: * @return boolean True if the line is an income line.
197: */
198: public static boolean isIncome(AccountingLine accountingLine) {
199: ObjectTypeService objectTypeService = (ObjectTypeService) SpringContext
200: .getBean(ObjectTypeService.class);
201: List<String> incomeObjectTypes = objectTypeService
202: .getCurrentYearBasicIncomeObjectTypes();
203: return incomeObjectTypes
204: .contains(getObjectCodeTypeCodeWithoutSideEffects(accountingLine));
205: }
206:
207: /**
208: * This populates an empty GeneralLedgerPendingEntry instance with default values for a bank offset. A global error will be
209: * posted as a side-effect if the given BankAccount has not defined the necessary bank offset relations.
210: *
211: * @param bankAccount
212: * @param depositAmount
213: * @param financialDocument
214: * @param universityFiscalYear
215: * @param sequenceHelper
216: * @param bankOffsetEntry
217: * @param errorPropertyName
218: * @return whether the entry was populated successfully
219: */
220: public static boolean populateBankOffsetGeneralLedgerPendingEntry(
221: BankAccount bankAccount, KualiDecimal depositAmount,
222: AccountingDocument financialDocument,
223: Integer universityFiscalYear,
224: GeneralLedgerPendingEntrySequenceHelper sequenceHelper,
225: GeneralLedgerPendingEntry bankOffsetEntry,
226: String errorPropertyName) {
227: bankOffsetEntry.setFinancialDocumentTypeCode(SpringContext
228: .getBean(DocumentTypeService.class)
229: .getDocumentTypeCodeByClass(
230: financialDocument.getClass()));
231: bankOffsetEntry.setVersionNumber(1L);
232: bankOffsetEntry
233: .setTransactionLedgerEntrySequenceNumber(sequenceHelper
234: .getSequenceCounter());
235: Timestamp transactionTimestamp = new Timestamp(SpringContext
236: .getBean(DateTimeService.class).getCurrentDate()
237: .getTime());
238: bankOffsetEntry.setTransactionDate(new java.sql.Date(
239: transactionTimestamp.getTime()));
240: bankOffsetEntry
241: .setTransactionEntryProcessedTs(new java.sql.Date(
242: transactionTimestamp.getTime()));
243: Account cashOffsetAccount = bankAccount.getCashOffsetAccount();
244: if (ObjectUtils.isNull(cashOffsetAccount)) {
245: // Bank account offsets are optional, so a BankAccount might not have the necessary relations. Validate them here where
246: // they are used so the need is clear.
247: // todo: put() varargs, revisit this in phase 2 when error reporting is discussed. -Leo
248: GlobalVariables
249: .getErrorMap()
250: .putError(
251: errorPropertyName,
252: KFSKeyConstants.ERROR_DOCUMENT_BANK_OFFSET_NO_ACCOUNT,
253: new String[] {
254: bankAccount
255: .getFinancialDocumentBankCode(),
256: bankAccount
257: .getFinDocumentBankAccountNumber() });
258: return false;
259: }
260: if (cashOffsetAccount.isAccountClosedIndicator()) {
261: GlobalVariables
262: .getErrorMap()
263: .putError(
264: errorPropertyName,
265: KFSKeyConstants.ERROR_DOCUMENT_BANK_OFFSET_ACCOUNT_CLOSED,
266: new String[] {
267: bankAccount
268: .getFinancialDocumentBankCode(),
269: bankAccount
270: .getFinDocumentBankAccountNumber(),
271: cashOffsetAccount
272: .getChartOfAccountsCode(),
273: cashOffsetAccount
274: .getAccountNumber() });
275: return false;
276: }
277: if (cashOffsetAccount.isExpired()) {
278: GlobalVariables
279: .getErrorMap()
280: .putError(
281: errorPropertyName,
282: KFSKeyConstants.ERROR_DOCUMENT_BANK_OFFSET_ACCOUNT_EXPIRED,
283: new String[] {
284: bankAccount
285: .getFinancialDocumentBankCode(),
286: bankAccount
287: .getFinDocumentBankAccountNumber(),
288: cashOffsetAccount
289: .getChartOfAccountsCode(),
290: cashOffsetAccount
291: .getAccountNumber() });
292: return false;
293: }
294: bankOffsetEntry.setChartOfAccountsCode(bankAccount
295: .getCashOffsetFinancialChartOfAccountCode());
296: bankOffsetEntry.setAccountNumber(bankAccount
297: .getCashOffsetAccountNumber());
298: bankOffsetEntry
299: .setFinancialDocumentApprovedCode(AccountingDocumentRuleBaseConstants.GENERAL_LEDGER_PENDING_ENTRY_CODE.NO);
300: bankOffsetEntry
301: .setTransactionEncumbranceUpdateCode(BLANK_SPACE);
302: bankOffsetEntry
303: .setFinancialBalanceTypeCode(BALANCE_TYPE_ACTUAL);
304: bankOffsetEntry.setTransactionDebitCreditCode(depositAmount
305: .isPositive() ? GL_DEBIT_CODE : GL_CREDIT_CODE);
306: bankOffsetEntry
307: .setFinancialSystemOriginationCode(SpringContext
308: .getBean(HomeOriginationService.class)
309: .getHomeOrigination()
310: .getFinSystemHomeOriginationCode());
311: bankOffsetEntry.setDocumentNumber(financialDocument
312: .getDocumentNumber());
313: ObjectCode cashOffsetObject = bankAccount.getCashOffsetObject();
314: if (ObjectUtils.isNull(cashOffsetObject)) {
315: // Bank account offsets are optional, so a BankAccount might not have the necessary relations. Validate them here where
316: // they are used so the need is clear.
317: // todo: put() varargs, revisit this in phase 2 when error reporting is discussed. -Leo
318: GlobalVariables
319: .getErrorMap()
320: .putError(
321: errorPropertyName,
322: KFSKeyConstants.ERROR_DOCUMENT_BANK_OFFSET_NO_OBJECT_CODE,
323: new String[] {
324: bankAccount
325: .getFinancialDocumentBankCode(),
326: bankAccount
327: .getFinDocumentBankAccountNumber() });
328: return false;
329: }
330: if (!cashOffsetObject.isFinancialObjectActiveCode()) {
331: GlobalVariables
332: .getErrorMap()
333: .putError(
334: errorPropertyName,
335: KFSKeyConstants.ERROR_DOCUMENT_BANK_OFFSET_INACTIVE_OBJECT_CODE,
336: new String[] {
337: bankAccount
338: .getFinancialDocumentBankCode(),
339: bankAccount
340: .getFinDocumentBankAccountNumber(),
341: cashOffsetObject
342: .getFinancialObjectCode() });
343: return false;
344: }
345: bankOffsetEntry.setFinancialObjectCode(bankAccount
346: .getCashOffsetObjectCode());
347: bankOffsetEntry.setFinancialObjectTypeCode(bankAccount
348: .getCashOffsetObject().getFinancialObjectTypeCode());
349: bankOffsetEntry.setOrganizationDocumentNumber(financialDocument
350: .getDocumentHeader().getOrganizationDocumentNumber());
351: bankOffsetEntry.setOrganizationReferenceId(null);
352: bankOffsetEntry.setProjectCode(KFSConstants
353: .getDashProjectCode());
354: bankOffsetEntry.setReferenceFinancialDocumentNumber(null);
355: bankOffsetEntry.setReferenceFinancialDocumentTypeCode(null);
356: bankOffsetEntry
357: .setReferenceFinancialSystemOriginationCode(null);
358: if (StringUtils.isBlank(bankAccount
359: .getCashOffsetSubAccountNumber())) {
360: bankOffsetEntry.setSubAccountNumber(KFSConstants
361: .getDashSubAccountNumber());
362: } else {
363: SubAccount cashOffsetSubAccount = bankAccount
364: .getCashOffsetSubAccount();
365: if (ObjectUtils.isNull(cashOffsetSubAccount)) {
366: GlobalVariables
367: .getErrorMap()
368: .putError(
369: errorPropertyName,
370: KFSKeyConstants.ERROR_DOCUMENT_BANK_OFFSET_NONEXISTENT_SUB_ACCOUNT,
371: new String[] {
372: bankAccount
373: .getFinancialDocumentBankCode(),
374: bankAccount
375: .getFinDocumentBankAccountNumber(),
376: cashOffsetAccount
377: .getChartOfAccountsCode(),
378: cashOffsetAccount
379: .getAccountNumber(),
380: bankAccount
381: .getCashOffsetSubAccountNumber() });
382: return false;
383: }
384: if (!cashOffsetSubAccount.isSubAccountActiveIndicator()) {
385: GlobalVariables
386: .getErrorMap()
387: .putError(
388: errorPropertyName,
389: KFSKeyConstants.ERROR_DOCUMENT_BANK_OFFSET_INACTIVE_SUB_ACCOUNT,
390: new String[] {
391: bankAccount
392: .getFinancialDocumentBankCode(),
393: bankAccount
394: .getFinDocumentBankAccountNumber(),
395: cashOffsetAccount
396: .getChartOfAccountsCode(),
397: cashOffsetAccount
398: .getAccountNumber(),
399: bankAccount
400: .getCashOffsetSubAccountNumber() });
401: return false;
402: }
403: bankOffsetEntry.setSubAccountNumber(bankAccount
404: .getCashOffsetSubAccountNumber());
405: }
406: if (StringUtils.isBlank(bankAccount
407: .getCashOffsetSubObjectCode())) {
408: bankOffsetEntry.setFinancialSubObjectCode(KFSConstants
409: .getDashFinancialSubObjectCode());
410: } else {
411: SubObjCd cashOffsetSubObject = bankAccount
412: .getCashOffsetSubObject();
413: if (ObjectUtils.isNull(cashOffsetSubObject)) {
414: GlobalVariables
415: .getErrorMap()
416: .putError(
417: errorPropertyName,
418: KFSKeyConstants.ERROR_DOCUMENT_BANK_OFFSET_NONEXISTENT_SUB_OBJ,
419: new String[] {
420: bankAccount
421: .getFinancialDocumentBankCode(),
422: bankAccount
423: .getFinDocumentBankAccountNumber(),
424: cashOffsetAccount
425: .getChartOfAccountsCode(),
426: cashOffsetAccount
427: .getAccountNumber(),
428: cashOffsetObject
429: .getFinancialObjectCode(),
430: bankAccount
431: .getCashOffsetSubObjectCode() });
432: return false;
433: }
434: if (!cashOffsetSubObject
435: .isFinancialSubObjectActiveIndicator()) {
436: GlobalVariables
437: .getErrorMap()
438: .putError(
439: errorPropertyName,
440: KFSKeyConstants.ERROR_DOCUMENT_BANK_OFFSET_INACTIVE_SUB_OBJ,
441: new String[] {
442: bankAccount
443: .getFinancialDocumentBankCode(),
444: bankAccount
445: .getFinDocumentBankAccountNumber(),
446: cashOffsetAccount
447: .getChartOfAccountsCode(),
448: cashOffsetAccount
449: .getAccountNumber(),
450: cashOffsetObject
451: .getFinancialObjectCode(),
452: bankAccount
453: .getCashOffsetSubObjectCode() });
454: return false;
455: }
456: bankOffsetEntry.setFinancialSubObjectCode(bankAccount
457: .getCashOffsetSubObjectCode());
458: }
459: bankOffsetEntry.setFinancialSubObjectCode(getEntryValue(
460: bankAccount.getCashOffsetSubObjectCode(), KFSConstants
461: .getDashFinancialSubObjectCode()));
462: bankOffsetEntry.setTransactionEntryOffsetIndicator(true);
463: bankOffsetEntry.setTransactionLedgerEntryAmount(depositAmount
464: .abs());
465: bankOffsetEntry.setUniversityFiscalPeriodCode(null); // null here, is assigned during batch or in specific document rule
466: // classes
467: bankOffsetEntry.setUniversityFiscalYear(universityFiscalYear);
468: // TODO wait for core budget year data structures to be put in place
469: // bankOffsetEntry.setBudgetYear(accountingLine.getBudgetYear());
470: // bankOffsetEntry.setBudgetYearFundingSourceCode(budgetYearFundingSourceCode);
471: bankOffsetEntry.setAcctSufficientFundsFinObjCd(SpringContext
472: .getBean(SufficientFundsService.class)
473: .getSufficientFundsObjectCode(
474: cashOffsetObject,
475: cashOffsetAccount
476: .getAccountSufficientFundsCode()));
477: return true;
478: }
479:
480: /**
481: * Check object code type to determine whether the accounting line is expense.
482: *
483: * @param accountingLine
484: * @return boolean True if the line is an expense line.
485: */
486: public static boolean isExpense(AccountingLine accountingLine) {
487: // return SpringContext.getBean(KualiConfigurationService.class).succeedsRule(KFSConstants.FINANCIAL_NAMESPACE,
488: // KUALI_TRANSACTION_PROCESSING_GLOBAL_RULES_SECURITY_GROUPING, APPLICATION_PARAMETER.EXPENSE_OBJECT_TYPE_CODES,
489: // getObjectCodeTypeCodeWithoutSideEffects(accountingLine) );
490: ObjectTypeService objectTypeService = (ObjectTypeService) SpringContext
491: .getBean(ObjectTypeService.class);
492: List<String> expenseObjectTypes = objectTypeService
493: .getCurrentYearBasicExpenseObjectTypes();
494: return expenseObjectTypes
495: .contains(getObjectCodeTypeCodeWithoutSideEffects(accountingLine));
496: }
497:
498: /**
499: * Makes sure that the objectCode attribute is fully populated b/c we are using proxying in our persistence layer.
500: *
501: * @param accountingLine
502: * @return the object type code of the object code of the given accounting line
503: */
504: public static String getObjectCodeTypeCodeWithoutSideEffects(
505: AccountingLine accountingLine) {
506: accountingLine.refreshReferenceObject("objectCode");
507: return accountingLine.getObjectCode()
508: .getFinancialObjectTypeCode();
509: }
510:
511: /**
512: * A helper method that checks the intended target value for null and empty strings. If the intended target value is not null or
513: * an empty string, it returns that value, ohterwise, it returns a backup value.
514: *
515: * @param targetValue
516: * @param backupValue
517: * @return String
518: */
519: private static String getEntryValue(String targetValue,
520: String backupValue) {
521: if (StringUtils.isNotBlank(targetValue)) {
522: return targetValue;
523: } else {
524: return backupValue;
525: }
526: }
527:
528: /**
529: * Gets the named property from KualiConfigurationService (i.e., from ApplicationResources.properties) and formats it with the
530: * given arguments (if any).
531: *
532: * @param propertyName
533: * @param arguments
534: * @return the formatted property (i.e., message), with any {@code {0}} replaced with the first argument, {@code {1}} with the
535: * second argument, etc.
536: */
537: public static String formatProperty(String propertyName,
538: Object... arguments) {
539: return MessageFormat.format(SpringContext.getBean(
540: KualiConfigurationService.class).getPropertyString(
541: propertyName), arguments);
542: }
543: }
|