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.kfs.KFSConstants.ACCOUNTING_LINE_ERRORS;
019: import static org.kuali.kfs.KFSConstants.AMOUNT_PROPERTY_NAME;
020: import static org.kuali.kfs.KFSConstants.ZERO;
021: import static org.kuali.kfs.KFSKeyConstants.ERROR_DOCUMENT_BALANCE_CONSIDERING_SOURCE_AND_TARGET_AMOUNTS;
022: import static org.kuali.kfs.KFSKeyConstants.ERROR_DOCUMENT_PC_TRANSACTION_TOTAL_ACCTING_LINE_TOTAL_NOT_EQUAL;
023: import static org.kuali.kfs.KFSKeyConstants.ERROR_ZERO_AMOUNT;
024:
025: import java.util.Arrays;
026: import java.util.Iterator;
027: import java.util.List;
028:
029: import org.apache.commons.lang.StringUtils;
030: import org.kuali.core.util.ErrorMap;
031: import org.kuali.core.util.GlobalVariables;
032: import org.kuali.core.util.KualiDecimal;
033: import org.kuali.core.util.ObjectUtils;
034: import org.kuali.core.workflow.service.KualiWorkflowDocument;
035: import org.kuali.kfs.KFSKeyConstants;
036: import org.kuali.kfs.KFSPropertyConstants;
037: import org.kuali.kfs.bo.AccountingLine;
038: import org.kuali.kfs.bo.SourceAccountingLine;
039: import org.kuali.kfs.bo.TargetAccountingLine;
040: import org.kuali.kfs.context.SpringContext;
041: import org.kuali.kfs.document.AccountingDocument;
042: import org.kuali.kfs.rules.AccountingDocumentRuleBase;
043: import org.kuali.kfs.service.ParameterEvaluator;
044: import org.kuali.kfs.service.ParameterService;
045: import org.kuali.module.financial.bo.ProcurementCardTargetAccountingLine;
046: import org.kuali.module.financial.bo.ProcurementCardTransactionDetail;
047: import org.kuali.module.financial.document.ProcurementCardDocument;
048: import org.kuali.workflow.KualiWorkflowUtils.RouteLevelNames;
049:
050: import edu.iu.uis.eden.exception.WorkflowException;
051:
052: /**
053: * Business rule(s) applicable to Procurement Card document.
054: */
055: public class ProcurementCardDocumentRule extends
056: AccountingDocumentRuleBase {
057:
058: /**
059: * This method inserts modifies the errorPath due to architecture variations necessary in the procurement card document
060: * design. Finally, this method calls the super method to complete the validation and business rule checks..
061: *
062: * @param transactionalDocument The document the accounting line being updated resides within.
063: * @param accountingLine The original accounting line.
064: * @param updatedAccoutingLine The updated version of the accounting line.
065: * @return True if the business rules all pass for the update, false otherwise.
066: *
067: * @see org.kuali.core.rule.UpdateAccountingLineRule#processUpdateAccountingLineBusinessRules(org.kuali.core.document.FinancialDocument,
068: * org.kuali.core.bo.AccountingLine, org.kuali.core.bo.AccountingLine)
069: */
070: @Override
071: public boolean processUpdateAccountingLineBusinessRules(
072: AccountingDocument transactionalDocument,
073: AccountingLine accountingLine,
074: AccountingLine updatedAccountingLine) {
075: fixErrorPath(transactionalDocument, accountingLine);
076:
077: return super .processUpdateAccountingLineBusinessRules(
078: transactionalDocument, accountingLine,
079: updatedAccountingLine);
080: }
081:
082: /**
083: * This method performs business rule checks on the accounting line being added to the document to ensure the accounting line
084: * is valid and appropriate for the document. Currently, this method validates the account number and object code
085: * associated with the new accounting line. Only target lines can be changed, so we only need to validate them.
086: *
087: * @param transactionalDocument The document the new line is being added to.
088: * @param accountingLine The new accounting line being added.
089: * @return True if the business rules all pass, false otherwise.
090: *
091: * @see org.kuali.module.financial.rules.FinancialDocumentRuleBase#processCustomAddAccountingLineBusinessRules(org.kuali.core.document.FinancialDocument,
092: * org.kuali.core.bo.AccountingLine)
093: */
094: @Override
095: protected boolean processCustomAddAccountingLineBusinessRules(
096: AccountingDocument transactionalDocument,
097: AccountingLine accountingLine) {
098: boolean allow = true;
099:
100: if (accountingLine instanceof ProcurementCardTargetAccountingLine) {
101: LOG.debug("validating accounting line # "
102: + accountingLine.getSequenceNumber());
103:
104: // Somewhat of an ugly hack... the goal is to have fixErrorPath run for cases where it's _not_ a new accounting line.
105: // If it is a new accounting line, all is well. But if it isn't then this might be a case where approve is called
106: // and we need to run validation with proper errorPath for that.
107: if (accountingLine.getSequenceNumber() != null) {
108: fixErrorPath(transactionalDocument, accountingLine);
109: }
110:
111: LOG.debug("beginning object code validation ");
112: allow = validateObjectCode(transactionalDocument,
113: accountingLine);
114:
115: LOG.debug("beginning account number validation ");
116: allow = allow
117: & validateAccountNumber(transactionalDocument,
118: accountingLine);
119:
120: LOG.debug("end validating accounting line, has errors: "
121: + allow);
122: }
123:
124: return allow;
125: }
126:
127: /**
128: * This method performs business rule checks on the accounting line being updated to the document to ensure the accounting line
129: * is valid and appropriate for the document. Only target lines can be changed, so we only need to validate them.
130: *
131: * @param transactionalDocument The document the accounting line being updated resides within.
132: * @param accountingLine The original accounting line.
133: * @param updatedAccoutingLine The updated version of the accounting line.
134: * @return True if the business rules all pass for the update, false otherwise.
135: *
136: * @see org.kuali.module.financial.rules.FinancialDocumentRuleBase#processCustomUpdateAccountingLineBusinessRules(org.kuali.core.document.FinancialDocument,
137: * org.kuali.core.bo.AccountingLine, org.kuali.core.bo.AccountingLine)
138: */
139: @Override
140: protected boolean processCustomUpdateAccountingLineBusinessRules(
141: AccountingDocument transactionalDocument,
142: AccountingLine accountingLine,
143: AccountingLine updatedAccountingLine) {
144: return processCustomAddAccountingLineBusinessRules(
145: transactionalDocument, updatedAccountingLine);
146: }
147:
148: /**
149: * This method performs business rule checks on the accounting line being provided to ensure the accounting line
150: * is valid and appropriate for the document. Only target lines can be changed, so we only need to validate them.
151: *
152: * @param transactionalDocument The document associated with the accounting line being validated.
153: * @param accountingLine The accounting line being validated.
154: * @return True if the business rules all pass, false otherwise.
155: *
156: * @see org.kuali.module.financial.rules.FinancialDocumentRuleBase#processCustomReviewAccountingLineBusinessRules(org.kuali.core.document.FinancialDocument,
157: * org.kuali.core.bo.AccountingLine)
158: */
159: @Override
160: protected boolean processCustomReviewAccountingLineBusinessRules(
161: AccountingDocument transactionalDocument,
162: AccountingLine accountingLine) {
163: return processCustomAddAccountingLineBusinessRules(
164: transactionalDocument, accountingLine);
165: }
166:
167: /**
168: * This method validates the object code for a given accounting line by checking the object codes restrictions,
169: * including any restrictions in parameters table. Additional validation checks include:
170: * <ul>
171: * <li>Confirm object code is not null.</li>
172: * <li>Confirm object code is active.</li>
173: * <li>Confirm object code is permitted for the list of merchant category codes.</li>
174: * <li>Confirm object code sub type is permitted for the list of merchant category codes.</li>
175: * </ul>
176: *
177: * @param transactionalDocument The transaction document to retrieve transactions from for validation.
178: * @param accountingLine The accounting line containing the object code to be validated.
179: * @return True if the object code is valid and permitted for this type of transaction, false otherwise.
180: */
181: public boolean validateObjectCode(
182: AccountingDocument transactionalDocument,
183: AccountingLine accountingLine) {
184: ProcurementCardDocument pcDocument = (ProcurementCardDocument) transactionalDocument;
185: ErrorMap errors = GlobalVariables.getErrorMap();
186:
187: String errorKey = KFSPropertyConstants.FINANCIAL_OBJECT_CODE;
188: boolean objectCodeAllowed = true;
189:
190: /* object code exist done in super, check we have a valid object */
191: if (ObjectUtils.isNull(accountingLine.getObjectCode())) {
192: return false;
193: }
194:
195: /* make sure object code is active */
196: if (!accountingLine.getObjectCode()
197: .isFinancialObjectActiveCode()) {
198: errors.putError(errorKey, KFSKeyConstants.ERROR_INACTIVE,
199: "object code");
200: objectCodeAllowed = false;
201: }
202:
203: /* get merchant category code (mcc) restriction from transaction */
204: String mccRestriction = "";
205: ProcurementCardTargetAccountingLine line = (ProcurementCardTargetAccountingLine) accountingLine;
206: List pcTransactions = pcDocument.getTransactionEntries();
207: for (Iterator iter = pcTransactions.iterator(); iter.hasNext();) {
208: ProcurementCardTransactionDetail transactionEntry = (ProcurementCardTransactionDetail) iter
209: .next();
210: if (transactionEntry
211: .getFinancialDocumentTransactionLineNumber()
212: .equals(
213: line
214: .getFinancialDocumentTransactionLineNumber())) {
215: mccRestriction = transactionEntry
216: .getProcurementCardVendor()
217: .getTransactionMerchantCategoryCode();
218: }
219: }
220:
221: if (StringUtils.isBlank(mccRestriction)) {
222: return objectCodeAllowed;
223: }
224:
225: /* check object code is in permitted list for merchant category code (mcc) */
226: if (objectCodeAllowed) {
227: ParameterEvaluator evaluator = SpringContext
228: .getBean(ParameterService.class)
229: .getParameterEvaluator(
230: ProcurementCardDocument.class,
231: ProcurementCardDocumentRuleConstants.VALID_OBJECTS_BY_MCC_CODE_PARM_NM,
232: ProcurementCardDocumentRuleConstants.INVALID_OBJECTS_BY_MCC_CODE_PARM_NM,
233: mccRestriction,
234: accountingLine.getFinancialObjectCode());
235: objectCodeAllowed = evaluator.evaluateAndAddError(
236: SourceAccountingLine.class,
237: KFSPropertyConstants.FINANCIAL_OBJECT_CODE);
238: }
239:
240: /* check object sub type is in permitted list for merchant category code (mcc) */
241: if (objectCodeAllowed) {
242: ParameterEvaluator evaluator = SpringContext
243: .getBean(ParameterService.class)
244: .getParameterEvaluator(
245: ProcurementCardDocument.class,
246: ProcurementCardDocumentRuleConstants.VALID_OBJ_SUB_TYPE_BY_MCC_CODE_PARM_NM,
247: ProcurementCardDocumentRuleConstants.INVALID_OBJ_SUB_TYPE_BY_MCC_CODE_PARM_NM,
248: mccRestriction,
249: accountingLine.getObjectCode()
250: .getFinancialObjectSubTypeCode());
251: objectCodeAllowed = evaluator.evaluateAndAddError(
252: SourceAccountingLine.class,
253: "objectCode.financialObjectSubTypeCode",
254: KFSPropertyConstants.FINANCIAL_OBJECT_CODE);
255: }
256: return objectCodeAllowed;
257: }
258:
259: /**
260: * This method validates the account number for a given accounting line by checking the object codes restrictions,
261: * including any restrictions in parameters table.
262: *
263: * @param transactionalDocument The transaction document to retrieve transactions from for validation.
264: * @param accountingLine The accounting line containing the account number to be validated.
265: * @return True if the account number is valid and permitted for this type of transaction, false otherwise.
266: */
267: public boolean validateAccountNumber(
268: AccountingDocument transactionalDocument,
269: AccountingLine accountingLine) {
270: ProcurementCardDocument pcDocument = (ProcurementCardDocument) transactionalDocument;
271: ErrorMap errors = GlobalVariables.getErrorMap();
272:
273: String errorKey = KFSPropertyConstants.ACCOUNT_NUMBER;
274: boolean accountNumberAllowed = true;
275:
276: /* account exist and object exist done in super, check we have a valid object */
277: if (ObjectUtils.isNull(accountingLine.getAccount())
278: || ObjectUtils.isNull(accountingLine.getObjectCode())) {
279: return false;
280: }
281:
282: return accountNumberAllowed;
283: }
284:
285: /**
286: * Overrides FinancialDocumentRuleBase.isDocumentBalanceValid() and changes the default debit/credit comparison
287: * to checking the target total against the total balance. If they don't balance, and error message is produced
288: * that is more appropriate for procurement card documents.
289: *
290: * @param transactionalDocument The document balance will be retrieved from.
291: * @return True if the document is balanced, false otherwise.
292: */
293: @Override
294: protected boolean isDocumentBalanceValid(
295: AccountingDocument transactionalDocument) {
296: ProcurementCardDocument pcDocument = (ProcurementCardDocument) transactionalDocument;
297:
298: KualiDecimal targetTotal = pcDocument.getTargetTotal();
299: KualiDecimal sourceTotal = pcDocument.getSourceTotal();
300:
301: boolean isValid = targetTotal.compareTo(sourceTotal) == 0;
302:
303: if (!isValid) {
304: GlobalVariables
305: .getErrorMap()
306: .putError(
307: ACCOUNTING_LINE_ERRORS,
308: ERROR_DOCUMENT_BALANCE_CONSIDERING_SOURCE_AND_TARGET_AMOUNTS,
309: new String[] { sourceTotal.toString(),
310: targetTotal.toString() });
311: }
312:
313: List<ProcurementCardTransactionDetail> pcTransactionEntries = pcDocument
314: .getTransactionEntries();
315:
316: for (ProcurementCardTransactionDetail pcTransactionDetail : pcTransactionEntries) {
317: isValid &= isTransactionBalanceValid(pcTransactionDetail);
318: }
319:
320: return isValid;
321: }
322:
323: /**
324: * This method validates the balance of the transaction given. A procurement card transaction is in balance if
325: * the total amount of the transaction equals the total of the target accounting lines corresponding to the transaction.
326: *
327: * @param pcTransaction The transaction detail used to retrieve the procurement card transaction and target accounting
328: * lines used to check for in balance.
329: * @return True if the amounts are equal and the transaction is in balance, false otherwise.
330: */
331: protected boolean isTransactionBalanceValid(
332: ProcurementCardTransactionDetail pcTransactionDetail) {
333: boolean inBalance = true;
334: KualiDecimal transAmount = pcTransactionDetail
335: .getTransactionTotalAmount();
336: List<ProcurementCardTargetAccountingLine> targetAcctingLines = pcTransactionDetail
337: .getTargetAccountingLines();
338:
339: KualiDecimal targetLineTotal = new KualiDecimal(0.00);
340:
341: for (TargetAccountingLine targetLine : targetAcctingLines) {
342: targetLineTotal = targetLineTotal.add(targetLine
343: .getAmount());
344: }
345:
346: // perform absolute value check because current system has situations where amounts may be opposite in sign
347: // This will no longer be necessary following completion of KULFDBCK-1290
348: inBalance = transAmount.abs().equals(targetLineTotal.abs());
349:
350: if (!inBalance) {
351: GlobalVariables
352: .getErrorMap()
353: .putError(
354: ACCOUNTING_LINE_ERRORS,
355: ERROR_DOCUMENT_PC_TRANSACTION_TOTAL_ACCTING_LINE_TOTAL_NOT_EQUAL,
356: new String[] { transAmount.toString(),
357: targetLineTotal.toString() });
358: }
359:
360: return inBalance;
361: }
362:
363: /**
364: * On procurement card documents, positive source amounts are credits, negative source amounts are debits.
365: *
366: * @param transactionalDocument The document the accounting line being checked is located in.
367: * @param accountingLine The accounting line being analyzed.
368: * @return True if the accounting line given is a debit accounting line, false otherwise.
369: * @throws Throws an IllegalStateException if one of the following rules are violated: the accounting line amount
370: * is zero or the accounting line is not an expense or income accounting line.
371: *
372: * @see org.kuali.module.financial.rules.FinancialDocumentRuleBase#isDebit(FinancialDocument, org.kuali.core.bo.AccountingLine)
373: * @see org.kuali.kfs.rules.AccountingDocumentRuleBase.IsDebitUtils#isDebitConsideringSection(AccountingDocumentRuleBase, AccountingDocument, AccountingLine)
374: */
375: public boolean isDebit(AccountingDocument transactionalDocument,
376: AccountingLine accountingLine) throws IllegalStateException {
377: // disallow error correction
378: IsDebitUtils.disallowErrorCorrectionDocumentCheck(this ,
379: transactionalDocument);
380: return IsDebitUtils.isDebitConsideringSection(this ,
381: transactionalDocument, accountingLine);
382: }
383:
384: /**
385: * This method determines if an account associated with the given accounting line is accessible (ie. editable).
386: *
387: * This method performs an additional check by looking at the status of the document passed in if the
388: * document is 'enroute' and there is an active route node equal to RouteLevelNames.ACCOUNT_REVIEW_FULL_EDIT,
389: * then the account is declared accessible. If the prior criteria are not met, then the method simply calls the super
390: * method and returns the results.
391: *
392: * @param transactionalDocument The document the accounting line is located in.
393: * @param accountingLine The accounting line which contains the account to be analyzed.
394: * @return True if the document is 'enroute' and will pass through the account review full edit node or if the super
395: * method returns true, false otherwise.
396: *
397: * @see org.kuali.module.financial.rules.FinancialDocumentRuleBase#accountIsAccessible(org.kuali.core.document.FinancialDocument,
398: * org.kuali.core.bo.AccountingLine)
399: */
400: @Override
401: protected boolean accountIsAccessible(
402: AccountingDocument transactionalDocument,
403: AccountingLine accountingLine) {
404: KualiWorkflowDocument workflowDocument = transactionalDocument
405: .getDocumentHeader().getWorkflowDocument();
406: List activeNodes = null;
407: try {
408: activeNodes = Arrays
409: .asList(workflowDocument.getNodeNames());
410: } catch (WorkflowException e) {
411: LOG.error("Error getting active nodes " + e.getMessage());
412: throw new RuntimeException("Error getting active nodes "
413: + e.getMessage());
414: }
415:
416: if (workflowDocument.stateIsEnroute()
417: && activeNodes
418: .contains(RouteLevelNames.ACCOUNT_REVIEW_FULL_EDIT)) {
419: return true;
420: }
421:
422: return super .accountIsAccessible(transactionalDocument,
423: accountingLine);
424: }
425:
426: /**
427: * This method validates an amount for an accounting line given. The following checks are performed to ensure
428: * validity:
429: * <ul>
430: * <li>Checks that an amount is not zero.</li>
431: * </ul>
432: *
433: * @param document The document that the accounting line being validated is contained within.
434: * @param accountingLine The accountingline the amount will be retrieved from.
435: * @return True if the amount is not zero, false otherwise.
436: *
437: * @see org.kuali.module.financial.rules.FinancialDocumentRuleBase#isAmountValid(org.kuali.core.document.FinancialDocument,
438: * org.kuali.core.bo.AccountingLine)
439: */
440: @Override
441: public boolean isAmountValid(AccountingDocument document,
442: AccountingLine accountingLine) {
443: KualiDecimal amount = accountingLine.getAmount();
444:
445: // Check for zero
446: if (ZERO.compareTo(amount) == 0) { // amount == 0
447: GlobalVariables.getErrorMap().putError(
448: AMOUNT_PROPERTY_NAME, ERROR_ZERO_AMOUNT,
449: "an accounting line");
450: LOG.info("failing isAmountValid - zero check");
451: return false;
452: }
453:
454: return true;
455: }
456:
457: /**
458: * This method is being overridden to avoid seeing ERROR_DOCUMENT_SINGLE_ACCOUNTING_LINE_SECTION_TOTAL_CHANGED
459: * error message on procurement card documents.
460: *
461: * @param propertyName The property name the error will be linked to.
462: * @param persistedSourceLineTotal The total amount that has already been persisted to the database.
463: * @param currentSourceLineTotal The new total amount being set.
464: */
465: @Override
466: protected void buildTotalChangeErrorMessage(String propertyName,
467: KualiDecimal persistedSourceLineTotal,
468: KualiDecimal currentSourceLineTotal) {
469: return;
470: }
471:
472: /**
473: * Fix the GlobalVariables.getErrorMap errorPath for how procurement card documents needs them in order
474: * to properly display errors on the interface. This is different from other financial document accounting
475: * lines because instead procurement card documents have accounting lines insides of transactions.
476: * Hence the error path is slightly different.
477: *
478: * @param financialDocument The financial document the errors will be posted to.
479: * @param accountingLine The accounting line the error will be posted on.
480: */
481: private void fixErrorPath(AccountingDocument financialDocument,
482: AccountingLine accountingLine) {
483: List transactionEntries = ((ProcurementCardDocument) financialDocument)
484: .getTransactionEntries();
485: ProcurementCardTargetAccountingLine targetAccountingLineToBeFound = (ProcurementCardTargetAccountingLine) accountingLine;
486:
487: String errorPath = KFSPropertyConstants.DOCUMENT;
488:
489: // originally I used getFinancialDocumentTransactionLineNumber to determine the appropriate transaction, unfortunately
490: // this makes it dependent on the order of transactionEntries in FP_PRCRMNT_DOC_T. Hence we have two loops below.
491: boolean done = false;
492: int transactionLineIndex = 0;
493: for (Iterator iterTransactionEntries = transactionEntries
494: .iterator(); !done && iterTransactionEntries.hasNext(); transactionLineIndex++) {
495: ProcurementCardTransactionDetail transactionEntry = (ProcurementCardTransactionDetail) iterTransactionEntries
496: .next();
497:
498: // Loop over the transactionEntry to find the accountingLine's location. Keep another counter handy.
499: int accountingLineCounter = 0;
500: for (Iterator iterTargetAccountingLines = transactionEntry
501: .getTargetAccountingLines().iterator(); !done
502: && iterTargetAccountingLines.hasNext(); accountingLineCounter++) {
503: ProcurementCardTargetAccountingLine targetAccountingLine = (ProcurementCardTargetAccountingLine) iterTargetAccountingLines
504: .next();
505:
506: if (targetAccountingLine.getSequenceNumber().equals(
507: targetAccountingLineToBeFound
508: .getSequenceNumber())) {
509: // Found the item, capture error path, and set boolean (break isn't enough for 2 loops).
510: errorPath = errorPath
511: + "."
512: + KFSPropertyConstants.TRANSACTION_ENTRIES
513: + "["
514: + transactionLineIndex
515: + "]."
516: + KFSPropertyConstants.TARGET_ACCOUNTING_LINES
517: + "[" + accountingLineCounter + "]";
518: done = true;
519: }
520: }
521: }
522:
523: if (!done) {
524: LOG
525: .warn("fixErrorPath failed to locate item accountingLine="
526: + accountingLine.toString());
527: }
528:
529: // Clearing the error path is not a universal solution but should work for PCDO. In this case it's the only choice
530: // because KualiRuleService.applyRules will miss to remove the previous transaction added error path (only this
531: // method knows how it is called).
532: ErrorMap errorMap = GlobalVariables.getErrorMap();
533: errorMap.clearErrorPath();
534: errorMap.addToErrorPath(errorPath);
535: }
536: }
|