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.purap.rules;
017:
018: import java.math.BigDecimal;
019: import java.util.Arrays;
020: import java.util.HashSet;
021: import java.util.List;
022: import java.util.Set;
023:
024: import org.apache.commons.lang.StringUtils;
025: import org.kuali.core.document.Document;
026: import org.kuali.core.rule.event.ApproveDocumentEvent;
027: import org.kuali.core.util.GlobalVariables;
028: import org.kuali.core.util.KualiDecimal;
029: import org.kuali.core.util.ObjectUtils;
030: import org.kuali.core.workflow.service.KualiWorkflowDocument;
031: import org.kuali.kfs.bo.AccountingLine;
032: import org.kuali.kfs.context.SpringContext;
033: import org.kuali.kfs.document.AccountingDocument;
034: import org.kuali.kfs.service.ParameterService;
035: import org.kuali.module.purap.PurapConstants;
036: import org.kuali.module.purap.PurapKeyConstants;
037: import org.kuali.module.purap.PurapPropertyConstants;
038: import org.kuali.module.purap.bo.PurApAccountingLine;
039: import org.kuali.module.purap.bo.PurApItem;
040: import org.kuali.module.purap.document.PurchasingAccountsPayableDocument;
041: import org.kuali.module.purap.rule.AddPurchasingAccountsPayableItemRule;
042:
043: import edu.iu.uis.eden.exception.WorkflowException;
044:
045: /**
046: * Business rule(s) applicable to Purchasing Accounts Payable Documents.
047: */
048: public class PurchasingAccountsPayableDocumentRuleBase extends
049: PurapAccountingDocumentRuleBase implements
050: AddPurchasingAccountsPayableItemRule {
051:
052: /**
053: * Overrides the method in PurapAccountingDocumentRuleBase to perform processValidation for PurchasingAccountsPayableDocument.
054: *
055: * @param document The PurchasingAccountsPayableDocument to be validated
056: * @return boolean true if it passes the validation
057: * @see org.kuali.module.purap.rules.PurapAccountingDocumentRuleBase#processCustomRouteDocumentBusinessRules(org.kuali.core.document.Document)
058: */
059: @Override
060: protected boolean processCustomRouteDocumentBusinessRules(
061: Document document) {
062: boolean isValid = true;
063: PurchasingAccountsPayableDocument purapDocument = (PurchasingAccountsPayableDocument) document;
064:
065: return isValid &= processValidation(purapDocument);
066: }
067:
068: /**
069: * Overrides the method in PurapAccountingDocumentRuleBase to perform processValidation for PurchasingAccountsPayableDocument.
070: *
071: * @param approveEvent The ApproveDocumentEvent instance that we can use to retrieve the document to be validated.
072: * @return boolean true if it passes the validation.
073: * @see org.kuali.module.purap.rules.PurapAccountingDocumentRuleBase#processCustomApproveDocumentBusinessRules(org.kuali.core.rule.event.ApproveDocumentEvent)
074: */
075: @Override
076: protected boolean processCustomApproveDocumentBusinessRules(
077: ApproveDocumentEvent approveEvent) {
078: boolean isValid = true;
079: PurchasingAccountsPayableDocument purapDocument = (PurchasingAccountsPayableDocument) approveEvent
080: .getDocument();
081:
082: return isValid &= processValidation(purapDocument);
083: }
084:
085: /**
086: * Overrides the method in PurapAccountingDocumentRuleBase to always return true.
087: *
088: * @param financialDocument The PurchasingAccountsPayableDocument to be validated.
089: * @param accountingLine The accounting line that is being added.
090: * @return boolean true.
091: * @see org.kuali.module.purap.rules.PurapAccountingDocumentRuleBase#processCustomAddAccountingLineBusinessRules(org.kuali.kfs.document.AccountingDocument,
092: * org.kuali.kfs.bo.AccountingLine)
093: */
094: @Override
095: protected boolean processCustomAddAccountingLineBusinessRules(
096: AccountingDocument financialDocument,
097: AccountingLine accountingLine) {
098: boolean isValid = true;
099:
100: return isValid;
101: }
102:
103: /**
104: * Overrides the method in PurapAccountingDocumentRuleBase to always return true.
105: *
106: * @param financialDocument The PurchasingAccountsPayableDocument to be validated.
107: * @param accountingLine The accounting line to be deleted.
108: * @param lineWasAlreadyDeletedFromDocument boolean true if the line was already deleted from document.
109: * @return boolean true.
110: * @see org.kuali.module.purap.rules.PurapAccountingDocumentRuleBase#processDeleteAccountingLineBusinessRules(org.kuali.kfs.document.AccountingDocument,
111: * org.kuali.kfs.bo.AccountingLine, boolean)
112: */
113: @Override
114: public boolean processDeleteAccountingLineBusinessRules(
115: AccountingDocument financialDocument,
116: AccountingLine accountingLine,
117: boolean lineWasAlreadyDeletedFromDocument) {
118: // I think PURAP's accounting line is a bit different than other documents. The source accounting line is per item, not per
119: // document.
120: // Besides, we already have other item validations that determined whether the items contain at least one account wherever
121: // applicable.
122: // So this will be redundant if we do another validation, therefore we'll return true here so that it would not give the
123: // error about
124: // can't delete the last remaining accessible account anymore.
125: return true;
126: }
127:
128: /**
129: * Calls each tab specific validation. Tabs included on all PURAP docs are: DocumentOverview, Vendor and Item
130: *
131: * @param purapDocument The PurchasingAccountsPayableDocument to be validated.
132: * @return boolean true if it passes all the validation.
133: */
134: public boolean processValidation(
135: PurchasingAccountsPayableDocument purapDocument) {
136: boolean valid = true;
137: valid &= processDocumentOverviewValidation(purapDocument);
138: valid &= processVendorValidation(purapDocument);
139: valid &= processItemValidation(purapDocument);
140:
141: return valid;
142: }
143:
144: /**
145: * Performs any validation for the Document Overview tab. Currently it will always return true.
146: *
147: * @param purapDocument The PurchasingAccountsPayable document to be validated.
148: * @return boolean true.
149: */
150: public boolean processDocumentOverviewValidation(
151: PurchasingAccountsPayableDocument purapDocument) {
152: boolean valid = true;
153: // currently, there is no validation to force at the PURAP level for this tab
154:
155: return valid;
156: }
157:
158: /**
159: * Performs any validation for the Vendor tab. Currently it will always return true.
160: *
161: * @param purapDocument The PurchasingAccountsPayable document to be validated.
162: * @return boolean true.
163: */
164: public boolean processVendorValidation(
165: PurchasingAccountsPayableDocument purapDocument) {
166: boolean valid = true;
167: // currently, there is no validation to force at the PURAP level for this tab
168:
169: return valid;
170: }
171:
172: /**
173: * Determines whether the document will require account validation to be done on all of its items.
174: *
175: * @param document The PurchasingAccountsPayable document to be validated.
176: * @return boolean true.
177: */
178: public boolean requiresAccountValidationOnAllEnteredItems(
179: PurchasingAccountsPayableDocument document) {
180:
181: return true;
182: }
183:
184: /**
185: * Performs any validation for the Item tab. For each item, it will invoke the data dictionary validations. If the item is
186: * considered entered, if the item type is above the line item, then also invoke the validatBelowTheLineValues. If the document
187: * requires account validation on all entered items or if the item contains accounting line, then call the
188: * processAccountValidation for all of the item's accounting line.
189: *
190: * @param purapDocument The PurchasingAccountsPayable document to be validated.
191: * @param needAccountValidation boolean that indicates whether we need account validation.
192: * @return boolean true if it passes all of the validations.
193: */
194: public boolean processItemValidation(
195: PurchasingAccountsPayableDocument purapDocument) {
196: boolean valid = true;
197:
198: // Fetch the business rules that are common to the below the line items on all purap documents
199: String documentTypeClassName = purapDocument.getClass()
200: .getName();
201: String[] documentTypeArray = StringUtils.split(
202: documentTypeClassName, ".");
203: String documentType = documentTypeArray[documentTypeArray.length - 1];
204: // If it's a credit memo, we'll have to append the source of the credit memo
205: // whether it's created from a Vendor, a PO or a PREQ.
206: if (documentType.equals("CreditMemoDocument")) {
207:
208: }
209:
210: boolean requiresAccountValidationOnAllEnteredItems = requiresAccountValidationOnAllEnteredItems(purapDocument);
211: int i = 0;
212: for (PurApItem item : purapDocument.getItems()) {
213: getDictionaryValidationService().validateBusinessObject(
214: item);
215: if (item.isConsideredEntered()) {
216: GlobalVariables.getErrorMap().addToErrorPath(
217: "document.item[" + i + "]");
218: // only do this check for below the line items
219: if (!item.getItemType()
220: .isItemTypeAboveTheLineIndicator()) {
221: valid &= validateBelowTheLineValues(documentType,
222: item);
223: }
224: GlobalVariables.getErrorMap().removeFromErrorPath(
225: "document.item[" + i + "]");
226:
227: if (requiresAccountValidationOnAllEnteredItems
228: || (!item.getSourceAccountingLines().isEmpty())) {
229: processAccountValidation(purapDocument, item
230: .getSourceAccountingLines(), item
231: .getItemIdentifierString());
232: }
233: }
234: i++;
235: }
236:
237: return valid;
238: }
239:
240: /**
241: * Performs validations for below the line items. If the unit price is zero, and the system parameter indicates that the item
242: * should not allow zero, then the validation fails. If the unit price is positive and the system parameter indicates that the
243: * item should not allow positive values, then the validation fails. If the unit price is negative and the system parameter
244: * indicates that the item should not allow negative values, then the validation fails. If the unit price is entered and is not
245: * zero and the item description is empty and the system parameter indicates that the item requires user to enter description,
246: * then the validation fails.
247: *
248: * @param documentType The type of the PurchasingAccountsPayable document to be validated.
249: * @param item The item to be validated.
250: * @return boolean true if it passes the validation.
251: */
252: protected boolean validateBelowTheLineValues(String documentType,
253: PurApItem item) {
254: boolean valid = true;
255: ParameterService parameterService = SpringContext
256: .getBean(ParameterService.class);
257: try {
258: if (ObjectUtils.isNotNull(item.getItemUnitPrice())
259: && (new KualiDecimal(item.getItemUnitPrice()))
260: .isZero()) {
261: if (parameterService
262: .parameterExists(
263: Class
264: .forName(PurapConstants.PURAP_DETAIL_TYPE_CODE_MAP
265: .get(documentType)),
266: PurapConstants.ITEM_ALLOWS_ZERO)
267: && !parameterService
268: .getParameterEvaluator(
269: Class
270: .forName(PurapConstants.PURAP_DETAIL_TYPE_CODE_MAP
271: .get(documentType)),
272: PurapConstants.ITEM_ALLOWS_ZERO,
273: item.getItemTypeCode())
274: .evaluationSucceeds()) {
275: valid = false;
276: GlobalVariables
277: .getErrorMap()
278: .putError(
279: PurapPropertyConstants.ITEM_UNIT_PRICE,
280: PurapKeyConstants.ERROR_ITEM_BELOW_THE_LINE,
281: item.getItemType()
282: .getItemTypeDescription(),
283: "zero");
284: }
285: } else if (ObjectUtils.isNotNull(item.getItemUnitPrice())
286: && (new KualiDecimal(item.getItemUnitPrice()))
287: .isPositive()) {
288: if (parameterService
289: .parameterExists(
290: Class
291: .forName(PurapConstants.PURAP_DETAIL_TYPE_CODE_MAP
292: .get(documentType)),
293: PurapConstants.ITEM_ALLOWS_POSITIVE)
294: && !parameterService
295: .getParameterEvaluator(
296: Class
297: .forName(PurapConstants.PURAP_DETAIL_TYPE_CODE_MAP
298: .get(documentType)),
299: PurapConstants.ITEM_ALLOWS_POSITIVE,
300: item.getItemTypeCode())
301: .evaluationSucceeds()) {
302: valid = false;
303: GlobalVariables
304: .getErrorMap()
305: .putError(
306: PurapPropertyConstants.ITEM_UNIT_PRICE,
307: PurapKeyConstants.ERROR_ITEM_BELOW_THE_LINE,
308: item.getItemType()
309: .getItemTypeDescription(),
310: "positive");
311: }
312: } else if (ObjectUtils.isNotNull(item.getItemUnitPrice())
313: && (new KualiDecimal(item.getItemUnitPrice()))
314: .isNegative()) {
315: if (parameterService
316: .parameterExists(
317: Class
318: .forName(PurapConstants.PURAP_DETAIL_TYPE_CODE_MAP
319: .get(documentType)),
320: PurapConstants.ITEM_ALLOWS_NEGATIVE)
321: && !parameterService
322: .getParameterEvaluator(
323: Class
324: .forName(PurapConstants.PURAP_DETAIL_TYPE_CODE_MAP
325: .get(documentType)),
326: PurapConstants.ITEM_ALLOWS_NEGATIVE,
327: item.getItemTypeCode())
328: .evaluationSucceeds()) {
329: valid = false;
330: GlobalVariables
331: .getErrorMap()
332: .putError(
333: PurapPropertyConstants.ITEM_UNIT_PRICE,
334: PurapKeyConstants.ERROR_ITEM_BELOW_THE_LINE,
335: item.getItemType()
336: .getItemTypeDescription(),
337: "negative");
338: }
339: }
340: if (ObjectUtils.isNotNull(item.getItemUnitPrice())
341: && (new KualiDecimal(item.getItemUnitPrice()))
342: .isNonZero()
343: && StringUtils.isEmpty(item.getItemDescription())) {
344: if (parameterService
345: .parameterExists(
346: Class
347: .forName(PurapConstants.PURAP_DETAIL_TYPE_CODE_MAP
348: .get(documentType)),
349: PurapConstants.ITEM_REQUIRES_USER_ENTERED_DESCRIPTION)
350: && parameterService
351: .getParameterEvaluator(
352: Class
353: .forName(PurapConstants.PURAP_DETAIL_TYPE_CODE_MAP
354: .get(documentType)),
355: PurapConstants.ITEM_REQUIRES_USER_ENTERED_DESCRIPTION,
356: item.getItemTypeCode())
357: .evaluationSucceeds()) {
358: // if
359: // (parameterService.getIndicatorParameter(Class.forName(PurapConstants.PURAP_DETAIL_TYPE_CODE_MAP.get(documentType)),
360: // PurapConstants.ITEM_REQUIRES_USER_ENTERED_DESCRIPTION)) {
361: valid = false;
362: GlobalVariables
363: .getErrorMap()
364: .putError(
365: PurapPropertyConstants.ITEM_DESCRIPTION,
366: PurapKeyConstants.ERROR_ITEM_BELOW_THE_LINE,
367: "The item description of "
368: + item
369: .getItemType()
370: .getItemTypeDescription(),
371: "empty");
372: }
373: }
374: } catch (ClassNotFoundException e) {
375: throw new RuntimeException(
376: "The valideBelowTheLineValues of PurchasingAccountsPayableDocumentRuleBase was unable to resolve a document type class: "
377: + PurapConstants.PURAP_DETAIL_TYPE_CODE_MAP
378: .get(documentType), e);
379: }
380:
381: return valid;
382: }
383:
384: /**
385: * Performs the data dictionary validation to validate whether the item is a valid business object.
386: *
387: * @param financialDocument The document containing the item to be validated.
388: * @param item The item to be validated.
389: * @return boolean true if it passes the validation.
390: * @see org.kuali.module.purap.rule.AddPurchasingAccountsPayableItemRule#processAddItemBusinessRules(org.kuali.kfs.document.AccountingDocument,
391: * org.kuali.module.purap.bo.PurApItem)
392: */
393: public boolean processAddItemBusinessRules(
394: AccountingDocument financialDocument, PurApItem item) {
395:
396: return getDictionaryValidationService().isBusinessObjectValid(
397: item, PurapPropertyConstants.NEW_PURCHASING_ITEM_LINE);
398: }
399:
400: /**
401: * A helper method for determining the route levels for a given document.
402: *
403: * @param workflowDocument The workflow document from which the current route levels are to be obtained.
404: * @return List The List of current route levels of the given document.
405: */
406: protected static List getCurrentRouteLevels(
407: KualiWorkflowDocument workflowDocument) {
408: try {
409: return Arrays.asList(workflowDocument.getNodeNames());
410: } catch (WorkflowException e) {
411: throw new RuntimeException(e);
412: }
413: }
414:
415: /**
416: * Determines whether the account is debit. It always returns false.
417: *
418: * @param financialDocument The document containing the account to be validated.
419: * @param accountingLine The account to be validated.
420: * @return boolean false.
421: * @see org.kuali.kfs.rule.AccountingLineRule#isDebit(org.kuali.kfs.document.AccountingDocument,
422: * org.kuali.kfs.bo.AccountingLine)
423: */
424: public boolean isDebit(AccountingDocument financialDocument,
425: AccountingLine accountingLine) {
426:
427: return false;
428: }
429:
430: /**
431: * Overrides the method in AccountingDocumentRuleBase to always return true.
432: *
433: * @param document The document to be validated.
434: * @param accountingLine The accounting line whose amount to be validated.
435: * @return boolean true.
436: * @see org.kuali.kfs.rules.AccountingDocumentRuleBase#isAmountValid(org.kuali.kfs.document.AccountingDocument,
437: * org.kuali.kfs.bo.AccountingLine)
438: */
439: @Override
440: public boolean isAmountValid(AccountingDocument document,
441: AccountingLine accountingLine) {
442:
443: return true;
444: }
445:
446: /**
447: * Performs any additional document level validation for the accounts which consists of validating that the item has accounts,
448: * the account percent is valid and the accounting strings are unique.
449: *
450: * @param purapDocument The document containing the accounts to be validated.
451: * @param purAccounts The List of accounts to be validated.
452: * @param itemLineNumber The string representing the item line number of the item whose accounts are to be validated.
453: * @return boolean true if it passes the validation.
454: */
455: public boolean processAccountValidation(
456: AccountingDocument accountingDocument,
457: List<PurApAccountingLine> purAccounts, String itemLineNumber) {
458: boolean valid = true;
459: valid = valid & verifyHasAccounts(purAccounts, itemLineNumber);
460: // if we don't have any accounts... not need to run any further validation as it will all fail
461: if (valid) {
462: valid = valid
463: & verifyAccountPercent(accountingDocument,
464: purAccounts, itemLineNumber);
465: }
466: // We can't invoke the verifyUniqueAccountingStrings in here because otherwise it would be invoking it more than once, if
467: // we're also
468: // calling it upon Save.
469: valid &= verifyUniqueAccountingStrings(purAccounts,
470: PurapConstants.ITEM_TAB_ERROR_PROPERTY, itemLineNumber);
471:
472: return valid;
473: }
474:
475: /**
476: * Verifies that the item has accounts.
477: *
478: * @param purAccounts The List of accounts to be validated.
479: * @param itemLineNumber The string representing the item line number of the item whose accounts are to be validated.
480: * @return boolean true if it passes the validation.
481: */
482: protected boolean verifyHasAccounts(
483: List<PurApAccountingLine> purAccounts, String itemLineNumber) {
484: boolean valid = true;
485:
486: if (purAccounts.isEmpty()) {
487: valid = false;
488: GlobalVariables.getErrorMap().putError(
489: PurapConstants.ITEM_TAB_ERROR_PROPERTY,
490: PurapKeyConstants.ERROR_ITEM_ACCOUNTING_INCOMPLETE,
491: itemLineNumber);
492: }
493:
494: return valid;
495: }
496:
497: /**
498: * Verifies account percent. If the total percent does not equal 100, the validation fails.
499: *
500: * @param accountingDocument The document containing the accounts to be validated.
501: * @param purAccounts The List of accounts to be validated.
502: * @param itemLineNumber The string representing the item line number of the item whose accounts are to be validated.
503: * @return boolean true if it passes the validation.
504: */
505: protected boolean verifyAccountPercent(
506: AccountingDocument accountingDocument,
507: List<PurApAccountingLine> purAccounts, String itemLineNumber) {
508: boolean valid = true;
509:
510: // validate that the percents total 100 for each item
511: BigDecimal totalPercent = BigDecimal.ZERO;
512: BigDecimal desiredPercent = new BigDecimal("100");
513: for (PurApAccountingLine account : purAccounts) {
514: if (account.getAccountLinePercent() != null) {
515: totalPercent = totalPercent.add(account
516: .getAccountLinePercent());
517: } else {
518: totalPercent = totalPercent.add(BigDecimal.ZERO);
519: }
520: }
521: if (desiredPercent.compareTo(totalPercent) != 0) {
522: GlobalVariables.getErrorMap().putError(
523: PurapConstants.ITEM_TAB_ERROR_PROPERTY,
524: PurapKeyConstants.ERROR_ITEM_ACCOUNTING_TOTAL,
525: itemLineNumber);
526: valid = false;
527: }
528:
529: return valid;
530: }
531:
532: /**
533: * Verifies that the accounting strings entered are unique for each item.
534: *
535: * @param purAccounts The List of accounts to be validated.
536: * @param errorPropertyName This is not currently being used in this method.
537: * @param itemLineNumber The string representing the item line number of the item whose accounts are to be validated.
538: * @return boolean true if it passes the validation.
539: */
540: protected boolean verifyUniqueAccountingStrings(
541: List<PurApAccountingLine> purAccounts,
542: String errorPropertyName, String itemLineNumber) {
543: Set existingAccounts = new HashSet();
544: for (PurApAccountingLine acct : purAccounts) {
545: if (!existingAccounts.contains(acct.toString())) {
546: existingAccounts.add(acct.toString());
547: } else {
548: GlobalVariables
549: .getErrorMap()
550: .putError(
551: PurapConstants.ITEM_TAB_ERROR_PROPERTY,
552: PurapKeyConstants.ERROR_ITEM_ACCOUNTING_NOT_UNIQUE,
553: itemLineNumber);
554: return false;
555: }
556: }
557:
558: return true;
559: }
560:
561: /**
562: * Verifies that the accounting strings are between 0 and 100 percent.
563: *
564: * @param account The account whose accounting string is to be validated.
565: * @param errorPropertyName The name of the property on the page that we want the error to be displayed.
566: * @param itemIdentifier The string representing the item whose account is being validated.
567: * @return boolean true if it passes the validation.
568: */
569: protected boolean verifyAccountingStringsBetween0And100Percent(
570: PurApAccountingLine account, String errorPropertyName,
571: String itemIdentifier) {
572: double pct = account.getAccountLinePercent().doubleValue();
573: if (pct <= 0 || pct > 100) {
574: GlobalVariables.getErrorMap().putError(errorPropertyName,
575: PurapKeyConstants.ERROR_ITEM_PERCENT, "%",
576: itemIdentifier);
577:
578: return false;
579: }
580:
581: return true;
582: }
583: }
|