/*
* The Kuali Financial System, a comprehensive financial management system for higher education.
*
* Copyright 2005-2014 The Kuali Foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kuali.kfs.sys.document;
import static org.kuali.kfs.sys.KualiTestAssertionUtils.assertEquality;
import static org.kuali.kfs.sys.KualiTestAssertionUtils.assertInequality;
import java.util.ArrayList;
import java.util.List;
import junit.framework.Assert;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.coa.businessobject.AccountingPeriod;
import org.kuali.kfs.coa.service.AccountingPeriodService;
import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
import org.kuali.kfs.sys.businessobject.TargetAccountingLine;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.datadictionary.FinancialSystemTransactionalDocumentEntry;
import org.kuali.kfs.sys.document.workflow.WorkflowTestUtils;
import org.kuali.kfs.sys.fixture.UserNameFixture;
import org.kuali.kfs.sys.monitor.ChangeMonitor;
import org.kuali.kfs.sys.monitor.DocumentVersionMonitor;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kew.api.document.DocumentStatus;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kns.service.DataDictionaryService;
import org.kuali.rice.kns.service.TransactionalDocumentDictionaryService;
import org.kuali.rice.krad.bo.AdHocRouteRecipient;
import org.kuali.rice.krad.datadictionary.DataDictionary;
import org.kuali.rice.krad.document.Copyable;
import org.kuali.rice.krad.document.Document;
import org.kuali.rice.krad.exception.ValidationException;
import org.kuali.rice.krad.service.DocumentService;
import org.kuali.rice.krad.util.ErrorMessage;
import org.kuali.rice.krad.util.GlobalVariables;
import org.kuali.rice.krad.util.ObjectUtils;
import org.kuali.rice.krad.workflow.service.WorkflowDocumentService;
public final class AccountingDocumentTestUtils {
private static final Logger LOG = Logger.getLogger(AccountingDocumentTestUtils.class);
protected static final int ROUTE_STATUS_CHANGE_WAIT_TIME_SECONDS = 30;
protected static final int ROUTE_STATUS_CHANGE_INITIAL_WAIT_TIME_SECONDS = 5;
public static void testAddAccountingLine(AccountingDocument document, List<SourceAccountingLine> sourceLines, List<TargetAccountingLine> targetLines, int expectedSourceTotal, int expectedTargetTotal) throws Exception {
Assert.assertTrue("expected count should be > 0", (expectedSourceTotal + expectedTargetTotal) > 0);
Assert.assertTrue("no lines found", (targetLines.size() + sourceLines.size()) > 0);
Assert.assertEquals("Document should have had no source accounting lines. Had: " + document.getSourceAccountingLines(), 0, document.getSourceAccountingLines().size());
Assert.assertEquals("Document should have had no target accounting lines. Had: " + document.getTargetAccountingLines(), 0, document.getTargetAccountingLines().size());
// add source lines
for (SourceAccountingLine sourceLine : sourceLines) {
document.addSourceAccountingLine(sourceLine);
}
// add target lines
for (TargetAccountingLine targetLine : targetLines) {
document.addTargetAccountingLine(targetLine);
}
Assert.assertEquals("source line count mismatch", expectedSourceTotal, document.getSourceAccountingLines().size());
Assert.assertEquals("target line count mismatch", expectedTargetTotal, document.getTargetAccountingLines().size());
}
public static <T extends AccountingDocument> void testGetNewDocument_byDocumentClass(Class<T> documentClass, DocumentService documentService) throws Exception {
T document = (T) documentService.getNewDocument(documentClass);
// verify document was created
Assert.assertNotNull("document was null",document);
Assert.assertNotNull("document header was null",document.getDocumentHeader());
Assert.assertNotNull("document number was null",document.getDocumentHeader().getDocumentNumber());
}
public static void testConvertIntoCopy_copyDisallowed(AccountingDocument document, DataDictionaryService dataDictionaryService) throws Exception {
// change the dataDictionary to disallow copying
DataDictionary d = dataDictionaryService.getDataDictionary();
Class documentClass = document.getClass();
boolean originalValue = d.getDocumentEntry(documentClass.getName()).getAllowsCopy();
try {
d.getDocumentEntry(documentClass.getName()).setAllowsCopy(false);
boolean failedAsExpected = false;
try {
((Copyable) document).toCopy();
}
catch (IllegalStateException e) {
failedAsExpected = true;
}
Assert.assertTrue("copy operation should have failed", failedAsExpected);
}
finally {
d.getDocumentEntry(documentClass.getName()).setAllowsCopy(originalValue);
}
}
public static void testConvertIntoErrorCorrection_documentAlreadyCorrected(AccountingDocument document, TransactionalDocumentDictionaryService dictionaryService) throws Exception {
if (((FinancialSystemTransactionalDocumentEntry)SpringContext.getBean(DataDictionaryService.class).getDataDictionary().getDocumentEntry(document.getClass().getName())).getAllowsErrorCorrection()) {
document.getFinancialSystemDocumentHeader().setCorrectedByDocumentId("1");
boolean failedAsExpected = false;
try {
((Correctable) document).toErrorCorrection();
}
catch (IllegalStateException e) {
failedAsExpected = true;
}
Assert.assertTrue("error correction should have failed", failedAsExpected);
}
}
public static void testConvertIntoErrorCorrection_errorCorrectionDisallowed(AccountingDocument document, DataDictionaryService dataDictionaryService) throws Exception {
// change the dataDictionary to disallow errorCorrection
DataDictionary d = dataDictionaryService.getDataDictionary();
Class documentClass = document.getClass();
boolean originalValue = ((FinancialSystemTransactionalDocumentEntry) d.getDocumentEntry(documentClass.getName())).getAllowsErrorCorrection();
try {
((FinancialSystemTransactionalDocumentEntry) d.getDocumentEntry(documentClass.getName())).setAllowsErrorCorrection(false);
boolean failedAsExpected = false;
try {
((Correctable) document).toErrorCorrection();
}
catch (IllegalStateException e) {
failedAsExpected = true;
}
Assert.assertTrue("error correction should have failed",failedAsExpected);
}
finally {
((FinancialSystemTransactionalDocumentEntry) d.getDocumentEntry(documentClass.getName())).setAllowsErrorCorrection(originalValue);
}
}
public static void testConvertIntoErrorCorrection_invalidYear(AccountingDocument document, TransactionalDocumentDictionaryService dictionaryService, AccountingPeriodService accountingPeriodService) throws Exception {
if (((FinancialSystemTransactionalDocumentEntry)SpringContext.getBean(DataDictionaryService.class).getDataDictionary().getDocumentEntry(document.getClass().getName())).getAllowsErrorCorrection()) {
// change to non-current posting year
Integer postingYear = document.getPostingYear();
AccountingPeriod accountingPeriod = accountingPeriodService.getByPeriod(document.getAccountingPeriod().getUniversityFiscalPeriodCode(), postingYear - 1);
Assert.assertNotNull("accounting period invalid for test", accountingPeriod);
Assert.assertTrue("accounting period invalid (same as current year)", postingYear != accountingPeriod.getUniversityFiscalYear());
Assert.assertEquals("accounting period invalid. period codes must remain the same", document.getAccountingPeriod().getUniversityFiscalPeriodCode(), accountingPeriod.getUniversityFiscalPeriodCode());
document.setAccountingPeriod(accountingPeriod);
boolean failedAsExpected = false;
try {
((Correctable) document).toErrorCorrection();
Assert.fail("converted into error correction for an invalid year");
}
catch (IllegalStateException e) {
failedAsExpected = true;
}
Assert.assertTrue(failedAsExpected);
}
}
/**
* @ShouldCommitTransactions needed for this test
* @see ShouldCommitTransactions
*/
public static void testRouteDocument(FinancialSystemTransactionalDocument document, DocumentService documentService) throws Exception {
document.prepareForSave();
Assert.assertFalse("Document was not in proper status for routing. Was: " + document.getDocumentHeader().getWorkflowDocument().getStatus(),
DocumentStatus.ENROUTE.equals(document.getDocumentHeader().getWorkflowDocument().getStatus()));
routeDocument(document, "saving copy source document", null, documentService);
WorkflowDocument workflowDocument = SpringContext.getBean(WorkflowDocumentService.class).loadWorkflowDocument(document.getDocumentNumber(), UserNameFixture.kfs.getPerson() );
if (!workflowDocument.isApproved()) {
WorkflowTestUtils.waitForStatusChange(document.getDocumentNumber(), DocumentStatus.ENROUTE);
}
}
/**
* @ShouldCommitTransactions needed for this test
* @see ShouldCommitTransactions
*/
public static void testConvertIntoErrorCorrection(AccountingDocument document, int expectedPrePECount, DocumentService documentService, TransactionalDocumentDictionaryService dictionaryService) throws Exception {
if (((FinancialSystemTransactionalDocumentEntry)SpringContext.getBean(DataDictionaryService.class).getDataDictionary().getDocumentEntry(document.getClass().getName())).getAllowsErrorCorrection()) {
String documentNumber = document.getDocumentNumber();
LOG.info("Submitting and blanket approving documentNumber to final to test error correction: " + documentNumber);
// route the original doc, wait for status change
blanketApproveDocument(document, "blanket approving errorCorrection source document", null, documentService);
WorkflowTestUtils.waitForDocumentApproval(document.getDocumentNumber());
// re-pull the document to get any updates made by KEW and the post-processor
document = (AccountingDocument) documentService.getByDocumentHeaderId(documentNumber);
// collect some preCorrect data
String preCorrectId = document.getDocumentNumber();
String preCorrectCorrectsId = document.getFinancialSystemDocumentHeader().getFinancialDocumentInErrorNumber();
int preCorrectPECount = document.getGeneralLedgerPendingEntries().size();
// int preCorrectNoteCount = document.getDocumentHeader().getNotes().size();
List<? extends SourceAccountingLine> preCorrectSourceLines = (List<? extends SourceAccountingLine>) ObjectUtils.deepCopy(new ArrayList<SourceAccountingLine>(document.getSourceAccountingLines()));
List<? extends TargetAccountingLine> preCorrectTargetLines = (List<? extends TargetAccountingLine>) ObjectUtils.deepCopy(new ArrayList<TargetAccountingLine>(document.getTargetAccountingLines()));
// validate preCorrect state
Assert.assertNotNull(preCorrectId);
Assert.assertNull(preCorrectCorrectsId);
Assert.assertEquals(expectedPrePECount, preCorrectPECount);
// assertEquals(0, preCorrectNoteCount);
// do the error correction
((Correctable) document).toErrorCorrection();
// compare to preCorrect state
String postCorrectId = document.getDocumentNumber();
LOG.info("postcorrect documentHeaderId = " + postCorrectId);
Assert.assertFalse(postCorrectId.equals(preCorrectId));
// pending entries should be cleared
int postCorrectPECount = document.getGeneralLedgerPendingEntries().size();
LOG.info("postcorrect PE count = " + postCorrectPECount);
Assert.assertEquals(0, postCorrectPECount);
// TODO: revisit this is it still needed
// // count 1 note, compare to "correction" text
// int postCorrectNoteCount = document.getDocumentHeader().getNotes().size();
// assertEquals(1, postCorrectNoteCount);
// DocumentNote note = document.getDocumentHeader().getNote(0);
// LOG.debug("postcorrect note text = " + note.getFinancialDocumentNoteText());
// assertTrue(note.getFinancialDocumentNoteText().indexOf("correction") != -1);
// correctsId should be equal to old id
String correctsId = document.getFinancialSystemDocumentHeader().getFinancialDocumentInErrorNumber();
LOG.info("postcorrect correctsId = " + correctsId);
Assert.assertEquals(preCorrectId, correctsId);
// accounting lines should have sign reversed on amounts
List<SourceAccountingLine> postCorrectSourceLines = document.getSourceAccountingLines();
Assert.assertEquals(preCorrectSourceLines.size(), postCorrectSourceLines.size());
for (int i = 0; i < preCorrectSourceLines.size(); ++i) {
SourceAccountingLine preCorrectLine = preCorrectSourceLines.get(i);
SourceAccountingLine postCorrectLine = postCorrectSourceLines.get(i);
LOG.info("postcorrect line(docId,amount) = " + i + "(" + postCorrectId + "," + postCorrectLine.getAmount());
assertEquality(postCorrectId, postCorrectLine.getDocumentNumber());
assertEquality(preCorrectLine.getAmount().negated(), postCorrectLine.getAmount());
}
List<? extends TargetAccountingLine> postCorrectTargetLines = document.getTargetAccountingLines();
Assert.assertEquals(preCorrectTargetLines.size(), postCorrectTargetLines.size());
for (int i = 0; i < preCorrectTargetLines.size(); ++i) {
TargetAccountingLine preCorrectLine = preCorrectTargetLines.get(i);
TargetAccountingLine postCorrectLine = postCorrectTargetLines.get(i);
LOG.info("postcorrect line(docId,amount) = " + i + "(" + postCorrectId + "," + postCorrectLine.getAmount());
assertEquality(postCorrectId, postCorrectLine.getDocumentNumber());
assertEquality(preCorrectLine.getAmount().negated(), postCorrectLine.getAmount());
}
}
}
/**
* @ShouldCommitTransactions needed for this test
* @see ShouldCommitTransactions
*/
public static void testSaveDocument(FinancialSystemTransactionalDocument document, DocumentService documentService) throws Exception {
// get document parameter
document.prepareForSave();
// save
saveDocument(document, documentService);
// retrieve
FinancialSystemTransactionalDocument result = (FinancialSystemTransactionalDocument) documentService.getByDocumentHeaderId(document.getDocumentNumber());
// verify
assertMatch(document, result);
}
/**
* @ShouldCommitTransactions needed for this test
* @see ShouldCommitTransactions
*/
public static void testConvertIntoCopy(AccountingDocument document, DocumentService documentService, int expectedPrePECount) throws Exception {
// save the original doc, wait for status change
document.prepareForSave();
routeDocument(document, "saving copy source document", null, documentService);
if (!document.getDocumentHeader().getWorkflowDocument().isApproved()) {
WorkflowTestUtils.waitForStatusChange(document.getDocumentNumber(), DocumentStatus.ENROUTE);
}
// collect some preCopy data
String preCopyId = document.getDocumentNumber();
String preCopyCopiedFromId = document.getDocumentHeader().getDocumentTemplateNumber();
int preCopyPECount = document.getGeneralLedgerPendingEntries().size();
// int preCopyNoteCount = document.getDocumentHeader().getNotes().size();
DocumentStatus preCopyStatus = document.getDocumentHeader().getWorkflowDocument().getStatus();
List<? extends SourceAccountingLine> preCopySourceLines = (List<? extends SourceAccountingLine>) ObjectUtils.deepCopy(new ArrayList(document.getSourceAccountingLines()));
List<? extends TargetAccountingLine> preCopyTargetLines = (List<? extends TargetAccountingLine>) ObjectUtils.deepCopy(new ArrayList(document.getTargetAccountingLines()));
// validate preCopy state
Assert.assertNotNull(preCopyId);
Assert.assertNull(preCopyCopiedFromId);
Assert.assertEquals(expectedPrePECount, preCopyPECount);
// do the copy
((Copyable) document).toCopy();
// compare to preCopy state
String postCopyId = document.getDocumentNumber();
Assert.assertFalse(postCopyId.equals(preCopyId));
// verify that docStatus has changed
DocumentStatus postCopyStatus = document.getDocumentHeader().getWorkflowDocument().getStatus();
Assert.assertFalse(postCopyStatus.equals(preCopyStatus));
// pending entries should be cleared
int postCopyPECount = document.getGeneralLedgerPendingEntries().size();
Assert.assertEquals(0, postCopyPECount);
// TODO: revisit this is it still needed
// count 1 note, compare to "copied" text
// int postCopyNoteCount = document.getDocumentHeader().getNotes().size();
// assertEquals(1, postCopyNoteCount);
// DocumentNote note = document.getDocumentHeader().getNote(0);
// assertTrue(note.getFinancialDocumentNoteText().indexOf("copied from") != -1);
// copiedFrom should be equal to old id
String copiedFromId = document.getDocumentHeader().getDocumentTemplateNumber();
Assert.assertEquals(preCopyId, copiedFromId);
// accounting lines should be have different docHeaderIds but same
// amounts
List<? extends SourceAccountingLine> postCopySourceLines = document.getSourceAccountingLines();
Assert.assertEquals(preCopySourceLines.size(), postCopySourceLines.size());
for (int i = 0; i < preCopySourceLines.size(); ++i) {
SourceAccountingLine preCopyLine = preCopySourceLines.get(i);
SourceAccountingLine postCopyLine = postCopySourceLines.get(i);
assertInequality(preCopyLine.getDocumentNumber(), postCopyLine.getDocumentNumber());
assertEquality(preCopyLine.getAmount(), postCopyLine.getAmount());
}
List<? extends TargetAccountingLine> postCopyTargetLines = document.getTargetAccountingLines();
Assert.assertEquals(preCopyTargetLines.size(), postCopyTargetLines.size());
for (int i = 0; i < preCopyTargetLines.size(); ++i) {
TargetAccountingLine preCopyLine = preCopyTargetLines.get(i);
TargetAccountingLine postCopyLine = postCopyTargetLines.get(i);
assertInequality(preCopyLine.getDocumentNumber(), postCopyLine.getDocumentNumber());
assertEquality(preCopyLine.getAmount(), postCopyLine.getAmount());
}
}
// helper methods
public static void routeDocument(FinancialSystemTransactionalDocument document, String annotation, List<AdHocRouteRecipient> adHocRoutingRecipients, DocumentService documentService) throws WorkflowException {
try {
documentService.routeDocument(document, annotation, adHocRoutingRecipients);
}
catch (ValidationException e) {
// If the business rule evaluation fails then give us more info for debugging this test.
Assert.fail(e.getMessage() + ", " + dumpMessageMapErrors());
}
}
// helper methods
public static void blanketApproveDocument(Document document, String annotation, List<AdHocRouteRecipient> adHocRoutingRecipients, DocumentService documentService) throws WorkflowException {
LOG.info( "Blanket Approving Document: " + document.getDocumentNumber() + " / " + annotation );
try {
documentService.blanketApproveDocument(document, annotation, adHocRoutingRecipients);
} catch (ValidationException e) {
// If the business rule evaluation fails then give us more info for debugging this test.
Assert.fail(e.getMessage() + ", " + dumpMessageMapErrors());
LOG.error( "Blanket Approval failed: " + document, e );
}
}
public static void approveDocument(AccountingDocument document, DocumentService documentService) throws Exception {
Long initialVersion = document.getVersionNumber();
documentService.approveDocument(document, "approving test doc", null);
DocumentVersionMonitor vm = new DocumentVersionMonitor(documentService, document.getDocumentNumber(), initialVersion);
Assert.assertTrue("Document did not complete routing to the expected status (" + vm + ") within the time limit",ChangeMonitor.waitUntilChange(vm, ROUTE_STATUS_CHANGE_WAIT_TIME_SECONDS, ROUTE_STATUS_CHANGE_INITIAL_WAIT_TIME_SECONDS));
}
public static void routeDocument(AccountingDocument document, DocumentService documentService) throws WorkflowException {
Assert.assertFalse("Document not in correct state before routing. Was: " + document.getDocumentHeader().getWorkflowDocument().getStatus(), DocumentStatus.ENROUTE.equals(document.getDocumentHeader().getWorkflowDocument().getStatus()));
documentService.routeDocument(document, "routing test doc", null);
WorkflowTestUtils.waitForStatusChange(document.getDocumentNumber(), DocumentStatus.ENROUTE);
}
public static void blanketApproveDocument(AccountingDocument document, DocumentService documentService) throws WorkflowException {
Assert.assertFalse("Document not in correct state before routing. Was: " + document.getDocumentHeader().getWorkflowDocument().getStatus(), DocumentStatus.ENROUTE.equals(document.getDocumentHeader().getWorkflowDocument().getStatus()));
documentService.blanketApproveDocument(document, "routing test doc", null);
WorkflowTestUtils.waitForDocumentApproval(document.getDocumentNumber());
}
public static void saveDocument(FinancialSystemTransactionalDocument document, DocumentService documentService) throws WorkflowException {
try {
documentService.saveDocument(document);
}
catch (ValidationException e) {
// If the business rule evaluation fails then give us more info for debugging this test.
Assert.fail("Document save failed with ValidationException: " + e.getMessage() + ", " + dumpMessageMapErrors());
}
}
public static void approve(String docHeaderId, UserNameFixture user, String expectedNode, DocumentService documentService) throws Exception {
WorkflowTestUtils.waitForApproveRequest(docHeaderId, GlobalVariables.getUserSession().getPerson());
Document document = documentService.getByDocumentHeaderId(docHeaderId);
Assert.assertTrue("Document should be at routing node " + expectedNode, WorkflowTestUtils.isAtNode(document, expectedNode));
Assert.assertTrue("Document should be enroute.", document.getDocumentHeader().getWorkflowDocument().isEnroute());
Assert.assertTrue(user + " should have an approve request.", document.getDocumentHeader().getWorkflowDocument().isApprovalRequested());
documentService.approveDocument(document, "Test approving as " + user, null);
}
public static <T extends Document> void assertMatch(T document1, T document2) {
Assert.assertEquals("Document number does not match", document1.getDocumentNumber(), document2.getDocumentNumber());
Assert.assertEquals("Document type does not match", document1.getDocumentHeader().getWorkflowDocument().getDocumentTypeName(), document2.getDocumentHeader().getWorkflowDocument().getDocumentTypeName());
AccountingDocument d1 = (AccountingDocument) document1;
AccountingDocument d2 = (AccountingDocument) document2;
if (StringUtils.isNotBlank(d1.getPostingPeriodCode()) && StringUtils.isNotBlank(d2.getPostingPeriodCode())) {
// some documents just plain old don't store this b/c the GLPEs get generated with "getCurrentAccountingPeriod()"
Assert.assertEquals("Posting Period does not match", d1.getPostingPeriodCode(), d2.getPostingPeriodCode());
}
Assert.assertEquals("Posting year does not match", d1.getPostingYear(), d2.getPostingYear());
Assert.assertEquals("Number of source accounting lines does not match", d1.getSourceAccountingLines().size(), d2.getSourceAccountingLines().size());
for (int i = 0; i < d1.getSourceAccountingLines().size(); i++) {
d1.getSourceAccountingLine(i).isLike(d2.getSourceAccountingLine(i));
}
Assert.assertEquals("Number of target accounting lines does not match", d1.getTargetAccountingLines().size(), d2.getTargetAccountingLines().size());
for (int i = 0; i < d1.getTargetAccountingLines().size(); i++) {
d1.getTargetAccountingLine(i).isLike(d2.getTargetAccountingLine(i));
}
}
protected static String dumpMessageMapErrors() {
if (GlobalVariables.getMessageMap().hasNoErrors()) {
return "";
}
StringBuilder message = new StringBuilder();
for ( String key : GlobalVariables.getMessageMap().getErrorMessages().keySet() ) {
List<ErrorMessage> errorList = GlobalVariables.getMessageMap().getErrorMessages().get(key);
for ( ErrorMessage em : errorList ) {
message.append(key).append(" = ").append( em.getErrorKey() );
if (em.getMessageParameters() != null) {
message.append( " : " );
String delim = "";
for ( String parm : em.getMessageParameters() ) {
message.append(delim).append("'").append(parm).append("'");
if ("".equals(delim)) {
delim = ", ";
}
}
}
}
message.append( '\n' );
}
return message.toString();
}
}