001: /*
002: * Copyright 2006-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.module.financial.rules;
017:
018: import static org.kuali.core.util.AssertionUtils.assertThat;
019:
020: import java.util.Arrays;
021: import java.util.Iterator;
022: import java.util.List;
023:
024: import org.apache.commons.lang.StringUtils;
025: import org.apache.log4j.Logger;
026: import org.kuali.core.bo.user.UniversalUser;
027: import org.kuali.core.document.Document;
028: import org.kuali.core.service.DictionaryValidationService;
029: import org.kuali.core.util.GeneralLedgerPendingEntrySequenceHelper;
030: import org.kuali.core.util.GlobalVariables;
031: import org.kuali.core.util.KualiDecimal;
032: import org.kuali.core.util.ObjectUtils;
033: import org.kuali.kfs.KFSConstants;
034: import org.kuali.kfs.KFSKeyConstants;
035: import org.kuali.kfs.KFSPropertyConstants;
036: import org.kuali.kfs.KFSConstants.DocumentStatusCodes.CashReceipt;
037: import org.kuali.kfs.bo.GeneralLedgerPendingEntry;
038: import org.kuali.kfs.context.SpringContext;
039: import org.kuali.kfs.document.AccountingDocument;
040: import org.kuali.kfs.rule.GenerateGeneralLedgerDocumentPendingEntriesRule;
041: import org.kuali.kfs.rules.AccountingDocumentRuleUtil;
042: import org.kuali.kfs.rules.GeneralLedgerPostingDocumentRuleBase;
043: import org.kuali.module.financial.bo.BankAccount;
044: import org.kuali.module.financial.bo.CashDrawer;
045: import org.kuali.module.financial.bo.Deposit;
046: import org.kuali.module.financial.bo.DepositCashReceiptControl;
047: import org.kuali.module.financial.document.CashManagementDocument;
048: import org.kuali.module.financial.document.CashReceiptDocument;
049: import org.kuali.module.financial.service.CashDrawerService;
050: import org.kuali.module.financial.service.CashManagementService;
051: import org.kuali.module.financial.service.CashReceiptService;
052: import org.kuali.module.financial.service.UniversityDateService;
053:
054: /**
055: * Business rule(s) applicable to Cash Management Document.
056: */
057: public class CashManagementDocumentRule extends
058: GeneralLedgerPostingDocumentRuleBase
059: implements
060: GenerateGeneralLedgerDocumentPendingEntriesRule<AccountingDocument> {
061: private static final Logger LOG = Logger
062: .getLogger(CashManagementDocumentRule.class);
063:
064: /**
065: * Overrides to validate that the person saving the document is the initiator, validates that the cash drawer is open for
066: * initial creation, validates that the cash drawer for the specific verification unit is closed for subsequent saves, and
067: * validates that the associate cash receipts are still verified.
068: *
069: * @param document submitted cash management document
070: * @return true if there are no issues processing rules associated with saving a cash management document
071: * @see org.kuali.core.rule.DocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.core.document.Document)
072: */
073: @Override
074: protected boolean processCustomSaveDocumentBusinessRules(
075: Document document) {
076: boolean isValid = super
077: .processCustomSaveDocumentBusinessRules(document);
078:
079: CashManagementDocument cmd = (CashManagementDocument) document;
080:
081: // verify the cash drawer for the verification unit is closed for post-initialized saves
082: verifyCashDrawerForVerificationUnitIsOpenForPostInitiationSaves(cmd);
083:
084: // verify deposits
085: isValid &= validateDeposits(cmd);
086:
087: return isValid;
088: }
089:
090: /**
091: * Overrides to validate that all cash receipts are deposited when routing cash management document.
092: *
093: * @param document submitted cash management document
094: * @return true if there are no issues processing rules associated with routing a cash management document
095: * @see org.kuali.core.rules.DocumentRuleBase#processCustomRouteDocumentBusinessRules(org.kuali.core.document.Document)
096: */
097: @Override
098: protected boolean processCustomRouteDocumentBusinessRules(
099: Document document) {
100: boolean isValid = true;
101:
102: CashManagementDocument cmDoc = (CashManagementDocument) document;
103: isValid &= verifyAllVerifiedCashReceiptsDeposited(cmDoc);
104:
105: return isValid;
106: }
107:
108: /**
109: * This method checks to make sure that the current system user is the person that initiated this document in the first place.
110: *
111: * @param cmd submitted cash management document
112: */
113: private void verifyUserIsDocumentInitiator(
114: CashManagementDocument cmd) {
115: UniversalUser currentUser = GlobalVariables.getUserSession()
116: .getUniversalUser();
117: if (cmd.getDocumentHeader() != null
118: && cmd.getDocumentHeader().getWorkflowDocument() != null) {
119: String cmdInitiatorNetworkId = cmd.getDocumentHeader()
120: .getWorkflowDocument().getInitiatorNetworkId();
121: if (!cmdInitiatorNetworkId.equalsIgnoreCase(currentUser
122: .getPersonUserIdentifier())) {
123: throw new IllegalStateException("The current user ("
124: + currentUser.getPersonUserIdentifier()
125: + ") is not the individual ("
126: + cmdInitiatorNetworkId
127: + ") that initiated this document.");
128: }
129: }
130: }
131:
132: /**
133: * This method checks to make sure that the cash drawer is closed for the associated verification unit, for post initiation
134: * saves for CashManagementDocuments which don't have Final
135: *
136: * @param cmd submitted cash management document
137: */
138: private void verifyCashDrawerForVerificationUnitIsOpenForPostInitiationSaves(
139: CashManagementDocument cmd) {
140: if (cmd.getDocumentHeader() != null
141: && cmd.getDocumentHeader().getWorkflowDocument() != null
142: && cmd.getDocumentHeader().getWorkflowDocument()
143: .getRouteHeader() != null) {
144: if (cmd.getDocumentHeader().getWorkflowDocument()
145: .stateIsSaved()) {
146: // now verify that the associated cash drawer is in the appropriate state
147: CashDrawer cd = SpringContext.getBean(
148: CashDrawerService.class).getByWorkgroupName(
149: cmd.getWorkgroupName(), true);
150: if (!cmd.hasFinalDeposit()) {
151: if (!cd.isOpen()) {
152: throw new IllegalStateException(
153: "The cash drawer for verification unit \""
154: + cd.getWorkgroupName()
155: + "\" is closed. It should be open when a cash management document for that verification unit is open and being saved.");
156: }
157: } else {
158: if (!cd.isLocked()) {
159: throw new IllegalStateException(
160: "The cash drawer for verification unit \""
161: + cd.getWorkgroupName()
162: + "\" is closed. It should be open when a cash management document for that verification unit is open and being saved.");
163: }
164: }
165: }
166: }
167: }
168:
169: /**
170: * Validates all Deposits associated with the given CashManagementDocument
171: *
172: * @param cmd submitted cash management document
173: * @return true if all deposits in a cash management are valid
174: */
175: private boolean validateDeposits(CashManagementDocument cmd) {
176: boolean isValid = true;
177: boolean isInitiated = cmd.getDocumentHeader()
178: .getWorkflowDocument().stateIsInitiated();
179:
180: GlobalVariables.getErrorMap().addToErrorPath(
181: KFSPropertyConstants.DOCUMENT);
182:
183: int index = 0;
184: for (Iterator deposits = cmd.getDeposits().iterator(); deposits
185: .hasNext(); index++) {
186: Deposit deposit = (Deposit) deposits.next();
187:
188: GlobalVariables.getErrorMap().addToErrorPath(
189: KFSPropertyConstants.DEPOSIT + "[" + index + "]");
190: isValid &= validateDeposit(deposit, isInitiated);
191: GlobalVariables.getErrorMap().removeFromErrorPath(
192: KFSPropertyConstants.DEPOSIT + "[" + index + "]");
193: }
194:
195: GlobalVariables.getErrorMap().removeFromErrorPath(
196: KFSPropertyConstants.DOCUMENT);
197:
198: return isValid;
199: }
200:
201: /**
202: * If documentIsInitiated, performs complete dataDictionary-driven validation of the given Deposit. Unconditionally validates
203: * the CashReceipts associated with the given Deposit.
204: *
205: * @param deposit individual deposit from cash management document
206: * @param documentIsInitiated if document is initiated
207: * @return true if deposit is valid
208: */
209: private boolean validateDeposit(Deposit deposit,
210: boolean documentIsInitiated) {
211: boolean isValid = true;
212:
213: verifyCashReceipts(deposit, documentIsInitiated);
214:
215: if (!documentIsInitiated) {
216: isValid = performDataDictionaryValidation(deposit);
217: }
218:
219: return isValid;
220: }
221:
222: private static final List INITIATED_STATES = Arrays
223: .asList(new String[] { CashReceipt.VERIFIED });
224: private static final List UNINITIATED_STATES = Arrays
225: .asList(new String[] { CashReceipt.INTERIM,
226: CashReceipt.FINAL });
227:
228: /**
229: * Verifies that all CashReceipts associated with the given document are of an appropriate status for the given
230: * CashManagementDocument state
231: *
232: * @param deposit deposit from cash management document
233: * @param documentIsInitiated if document is initiated
234: */
235: private void verifyCashReceipts(Deposit deposit,
236: boolean documentIsInitiated) {
237: List desiredCRStates = null;
238: if (documentIsInitiated) {
239: desiredCRStates = INITIATED_STATES;
240: } else {
241: desiredCRStates = UNINITIATED_STATES;
242: }
243:
244: for (Iterator depositCashReceiptControls = deposit
245: .getDepositCashReceiptControl().iterator(); depositCashReceiptControls
246: .hasNext();) {
247: DepositCashReceiptControl depositCashReceiptControl = (DepositCashReceiptControl) depositCashReceiptControls
248: .next();
249: CashReceiptDocument cashReceipt = depositCashReceiptControl
250: .getCashReceiptHeader().getCashReceiptDocument();
251: String crState = cashReceipt.getDocumentHeader()
252: .getFinancialDocumentStatusCode();
253: if (!desiredCRStates.contains(crState)) {
254: throw new IllegalStateException(
255: "Cash receipt document number "
256: + cashReceipt.getDocumentNumber()
257: + " is not in an appropriate state for the associated CashManagementDocument to be submitted.");
258: }
259: }
260: }
261:
262: /**
263: * Verifies that all verified cash receipts have been deposited
264: *
265: * @param cmDoc the cash management document that is about to be routed
266: * @return true if there are no outstanding verified cash receipts that are not part of a deposit, false if otherwise
267: */
268: private boolean verifyAllVerifiedCashReceiptsDeposited(
269: CashManagementDocument cmDoc) {
270: boolean allCRsDeposited = true;
271: CashManagementService cms = SpringContext
272: .getBean(CashManagementService.class);
273: List verifiedReceipts = SpringContext.getBean(
274: CashReceiptService.class).getCashReceipts(
275: cmDoc.getWorkgroupName(),
276: KFSConstants.DocumentStatusCodes.CashReceipt.VERIFIED);
277: for (Object o : verifiedReceipts) {
278: if (!cms.verifyCashReceiptIsDeposited(cmDoc,
279: (CashReceiptDocument) o)) {
280: allCRsDeposited = false;
281: GlobalVariables
282: .getErrorMap()
283: .putError(
284: KFSConstants.CASH_MANAGEMENT_DEPOSIT_ERRORS,
285: KFSKeyConstants.CashManagement.ERROR_NON_DEPOSITED_VERIFIED_CASH_RECEIPT,
286: new String[] { ((CashReceiptDocument) o)
287: .getDocumentNumber() });
288: }
289: }
290: return allCRsDeposited;
291: }
292:
293: /**
294: * Performs complete, recursive dataDictionary-driven validation of the given Deposit.
295: *
296: * @param deposit deposit from cash management document
297: * @return true if deposit is validated against data dictionary entry
298: */
299: private boolean performDataDictionaryValidation(Deposit deposit) {
300: // check for required fields
301: SpringContext.getBean(DictionaryValidationService.class)
302: .validateBusinessObject(deposit);
303:
304: // validate foreign-key relationships
305: deposit.refresh();
306:
307: // only check for BankAccount if both bankCode and bankAccountNumber are present
308: if (StringUtils.isNotBlank(deposit.getDepositBankCode())
309: && StringUtils.isNotBlank(deposit
310: .getDepositBankAccountNumber())) {
311: BankAccount bankAccount = deposit.getBankAccount();
312: if (ObjectUtils.isNull(bankAccount)) {
313: GlobalVariables
314: .getErrorMap()
315: .putError(
316: KFSPropertyConstants.DEPOSIT_BANK_ACCOUNT_NUMBER,
317: KFSKeyConstants.ERROR_EXISTENCE,
318: "Bank Account");
319: }
320: }
321:
322: return GlobalVariables.getErrorMap().isEmpty();
323: }
324:
325: /**
326: * Generates bank offset GLPEs for deposits, if enabled.
327: *
328: * @param financialDocument submitted accounting document
329: * @param sequenceHelper helper class to keep track of sequence of general ledger pending entries
330: * @return true if bank offset GLPE's for deposits are generated successfully
331: * @see org.kuali.kfs.rule.GenerateGeneralLedgerDocumentPendingEntriesRule#processGenerateDocumentGeneralLedgerPendingEntries(org.kuali.kfs.document.GeneralLedgerPostingDocument,
332: * org.kuali.core.util.GeneralLedgerPendingEntrySequenceHelper)
333: */
334: public boolean processGenerateDocumentGeneralLedgerPendingEntries(
335: AccountingDocument financialDocument,
336: GeneralLedgerPendingEntrySequenceHelper sequenceHelper) {
337: boolean success = true;
338: final CashManagementDocument cashManagementDocument = ((CashManagementDocument) financialDocument);
339: if (cashManagementDocument.isBankCashOffsetEnabled()) {
340: Integer universityFiscalYear = getUniversityFiscalYear();
341: int interimDepositNumber = 1;
342: for (Iterator iterator = cashManagementDocument
343: .getDeposits().iterator(); iterator.hasNext();) {
344: // todo: getDeposits() should return List<Deposit> not List
345: Deposit deposit = (Deposit) iterator.next();
346: deposit
347: .refreshReferenceObject(KFSPropertyConstants.BANK_ACCOUNT);
348:
349: GeneralLedgerPendingEntry bankOffsetEntry = new GeneralLedgerPendingEntry();
350: if (!AccountingDocumentRuleUtil
351: .populateBankOffsetGeneralLedgerPendingEntry(
352: deposit.getBankAccount(),
353: deposit.getDepositAmount(),
354: cashManagementDocument,
355: universityFiscalYear,
356: sequenceHelper,
357: bankOffsetEntry,
358: KFSConstants.CASH_MANAGEMENT_DEPOSIT_ERRORS)) {
359: success = false;
360: LOG.warn("Skipping ledger entries for depost "
361: + deposit.getDepositTicketNumber() + ".");
362: continue; // An unsuccessfully populated bank offset entry may contain invalid relations, so don't add it at
363: // all.
364: }
365: bankOffsetEntry
366: .setTransactionLedgerEntryDescription(createDescription(
367: deposit, interimDepositNumber++));
368: cashManagementDocument.getGeneralLedgerPendingEntries()
369: .add(bankOffsetEntry);
370: sequenceHelper.increment();
371:
372: GeneralLedgerPendingEntry offsetEntry = (GeneralLedgerPendingEntry) ObjectUtils
373: .deepCopy(bankOffsetEntry);
374: success &= populateOffsetGeneralLedgerPendingEntry(
375: universityFiscalYear, bankOffsetEntry,
376: sequenceHelper, offsetEntry);
377: cashManagementDocument.getGeneralLedgerPendingEntries()
378: .add(offsetEntry);
379: sequenceHelper.increment();
380: /*
381: * Only the final deposit will have non-null currency and coin. If this is the final deposit, generate the ledger
382: * entries for currency and coin.
383: */
384: if (deposit
385: .getDepositTypeCode()
386: .equals(
387: KFSConstants.DocumentStatusCodes.CashReceipt.FINAL)) {
388: KualiDecimal totalCoinCurrencyAmount = deposit
389: .getDepositedCurrency().getTotalAmount()
390: .add(
391: deposit.getDepositedCoin()
392: .getTotalAmount());
393: GeneralLedgerPendingEntry coinCurrencyBankOffsetEntry = new GeneralLedgerPendingEntry();
394: if (!AccountingDocumentRuleUtil
395: .populateBankOffsetGeneralLedgerPendingEntry(
396: deposit.getBankAccount(),
397: totalCoinCurrencyAmount,
398: cashManagementDocument,
399: universityFiscalYear,
400: sequenceHelper,
401: coinCurrencyBankOffsetEntry,
402: KFSConstants.CASH_MANAGEMENT_DEPOSIT_ERRORS)) {
403: success = false;
404: // An unsuccessfully populated bank offset entry may contain invalid relations, so don't add it at all.
405: LOG
406: .warn("Skipping ledger entries for coin and currency.");
407: continue;
408: }
409:
410: coinCurrencyBankOffsetEntry
411: .setTransactionLedgerEntryDescription(createDescription(
412: deposit, interimDepositNumber++));
413: cashManagementDocument
414: .getGeneralLedgerPendingEntries().add(
415: coinCurrencyBankOffsetEntry);
416: sequenceHelper.increment();
417:
418: GeneralLedgerPendingEntry coinCurrnecyOffsetEntry = (GeneralLedgerPendingEntry) ObjectUtils
419: .deepCopy(coinCurrencyBankOffsetEntry);
420: success &= populateOffsetGeneralLedgerPendingEntry(
421: universityFiscalYear,
422: coinCurrencyBankOffsetEntry,
423: sequenceHelper, coinCurrnecyOffsetEntry);
424: cashManagementDocument
425: .getGeneralLedgerPendingEntries().add(
426: coinCurrnecyOffsetEntry);
427: sequenceHelper.increment();
428:
429: }
430:
431: }
432:
433: }
434: return success;
435: }
436:
437: /**
438: * Create description for deposit
439: *
440: * @param deposit deposit from cash management document
441: * @param interimDepositNumber
442: * @return the description for the given deposit's GLPE bank offset
443: */
444: private static String createDescription(Deposit deposit,
445: int interimDepositNumber) {
446: String descriptionKey;
447: if (KFSConstants.DepositConstants.DEPOSIT_TYPE_FINAL
448: .equals(deposit.getDepositTypeCode())) {
449: descriptionKey = KFSKeyConstants.CashManagement.DESCRIPTION_GLPE_BANK_OFFSET_FINAL;
450: } else {
451: assertThat(
452: KFSConstants.DepositConstants.DEPOSIT_TYPE_INTERIM
453: .equals(deposit.getDepositTypeCode()),
454: deposit.getDepositTypeCode());
455: descriptionKey = KFSKeyConstants.CashManagement.DESCRIPTION_GLPE_BANK_OFFSET_INTERIM;
456: }
457: return AccountingDocumentRuleUtil.formatProperty(
458: descriptionKey, interimDepositNumber);
459: }
460:
461: /**
462: * Gets the fiscal year for the GLPEs generated by this document. This works the same way as in TransactionalDocumentBase. The
463: * property is down in TransactionalDocument because no FinancialDocument (currently only CashManagementDocument) allows the
464: * user to override it. So, that logic is duplicated here. A comment in TransactionalDocumentBase says that this implementation
465: * is a hack right now because it's intended to be set by the
466: * <code>{@link org.kuali.module.chart.service.AccountingPeriodService}</code>, which suggests to me that pulling that
467: * property up to FinancialDocument is preferable to duplicating this logic here.
468: *
469: * @return the fiscal year for the GLPEs generated by this document
470: */
471: private Integer getUniversityFiscalYear() {
472: return SpringContext.getBean(UniversityDateService.class)
473: .getCurrentFiscalYear();
474: }
475: }
|