/*
* 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.fp.document.service.impl;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.kuali.kfs.fp.businessobject.Check;
import org.kuali.kfs.fp.document.CashReceiptDocument;
import org.kuali.kfs.fp.document.service.CashReceiptCoverSheetService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.WorkflowDocument;
import org.kuali.rice.kns.service.DataDictionaryService;
import org.kuali.rice.kns.service.DocumentHelperService;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.AcroFields;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfImportedPage;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfStamper;
import com.lowagie.text.pdf.PdfWriter;
/**
* Implementation of service for handling creation of the cover sheet of the <code>{@link CashReceiptDocument}</code>
*/
public class CashReceiptCoverSheetServiceImpl implements CashReceiptCoverSheetService {
private static Log LOG = LogFactory.getLog(CashReceiptCoverSheetService.class);
private DataDictionaryService dataDictionaryService;
private DocumentHelperService documentHelperService;
public static final String CR_COVERSHEET_TEMPLATE_NM = "CashReceiptCoverSheetTemplate.pdf";
private static final float LEFT_MARGIN = 45;
private static final float TOP_MARGIN = 45;
private static final float TOP_FIRST_PAGE = 440;
private static final String DOCUMENT_NUMBER_FIELD = "DocumentNumber";
private static final String INITIATOR_FIELD = "Initiator";
private static final String CREATED_DATE_FIELD = "CreatedDate";
private static final String AMOUNT_FIELD = "Amount";
private static final String ORG_DOC_NUMBER_FIELD = "OrgDocNumber";
private static final String CAMPUS_FIELD = "Campus";
private static final String DEPOSIT_DATE_FIELD = "DepositDate";
private static final String DESCRIPTION_FIELD = "Description";
private static final String EXPLANATION_FIELD = "Explanation";
private static final String CHECKS_FIELD = "Checks";
private static final String CURRENCY_FIELD = "Currency";
private static final String COIN_FIELD = "Coin";
private static final String CASH_IN_FIELD = "CashIn";
private static final String MONEY_IN_FIELD = "MoneyIn";
private static final String CHANGE_CURRENCY_FIELD = "ChangeCurrency";
private static final String CHANGE_COIN_FIELD = "ChangeCoin";
private static final String CHANGE_OUT_FIELD = "ChangeOut";
private static final String RECONCILIATION_TOTAL_FIELD = "ReconciliationTotal";
// CM signature field needs to be added to the pdf template, but doesn't need to be populated, so not adding it here
private static final int FRONT_PAGE = 1;
private static final int CHECK_PAGE_NORMAL = 2;
private static final float CHECK_DETAIL_HEADING_HEIGHT = 45;
private static final float CHECK_LINE_SPACING = 12;
private static final float CHECK_FIELD_MARGIN = 12;
private static final float CHECK_NORMAL_FIELD_LENGTH = 100;
private static final float CHECK_FIELD_HEIGHT = 10;
private static final int MAX_CHECKS_FIRST_PAGE = 30;
private static final int MAX_CHECKS_NORMAL = 65;
private static final float CHECK_HEADER_HEIGHT = 12;
private static final String CHECK_NUMBER_FIELD_PREFIX = "CheckNumber";
private static final float CHECK_NUMBER_FIELD_POSITION = LEFT_MARGIN;
private static final String CHECK_DATE_FIELD_PREFIX = "CheckDate";
private static final float CHECK_DATE_FIELD_POSITION = CHECK_NUMBER_FIELD_POSITION + CHECK_NORMAL_FIELD_LENGTH + CHECK_FIELD_MARGIN;
private static final String CHECK_DESCRIPTION_FIELD_PREFIX = "CheckDescription";
private static final float CHECK_DESCRIPTION_FIELD_POSITION = CHECK_DATE_FIELD_POSITION + CHECK_NORMAL_FIELD_LENGTH + CHECK_FIELD_MARGIN;
private static final float CHECK_DESCRIPTION_FIELD_LENGTH = 250;
private static final String CHECK_AMOUNT_FIELD_PREFIX = "CheckAmount";
private static final float CHECK_AMOUNT_FIELD_POSITION = CHECK_DESCRIPTION_FIELD_POSITION + CHECK_DESCRIPTION_FIELD_LENGTH + CHECK_FIELD_MARGIN;
private float _yPos;
/**
* This method determines if cover sheet printing is allowed by reviewing the CashReceiptDocumentRule to see if the
* cover sheet is printable.
*
* @param crDoc The document the cover sheet is being printed for.
* @return True if the cover sheet is printable, false otherwise.
*
* @see org.kuali.kfs.fp.document.service.CashReceiptCoverSheetService#isCoverSheetPrintingAllowed(org.kuali.kfs.fp.document.CashReceiptDocument)
* @see org.kuali.kfs.fp.document.validation.impl.CashReceiptDocumentRule#isCoverSheetPrintable(org.kuali.kfs.fp.document.CashReceiptFamilyBase)
*/
@Override
public boolean isCoverSheetPrintingAllowed(CashReceiptDocument crDoc) {
WorkflowDocument workflowDocument = crDoc.getDocumentHeader().getWorkflowDocument();
return !(workflowDocument.isCanceled() || workflowDocument.isInitiated() || workflowDocument.isDisapproved() ||
workflowDocument.isException() || workflowDocument.isSaved());
}
/**
* Generate a cover sheet for the <code>{@link CashReceiptDocument}</code>. An <code>{@link OutputStream}</code> is written
* to for the cover sheet.
*
* @param document The cash receipt document the cover sheet is for.
* @param searchPath The directory path to the template to be used to generate the cover sheet.
* @param returnStream The output stream the cover sheet will be written to.
* @exception DocumentException Thrown if the document provided is invalid, including null.
* @exception IOException Thrown if there is a problem writing to the output stream.
* @see org.kuali.rice.kns.module.financial.service.CashReceiptCoverSheetServiceImpl#generateCoverSheet(
* org.kuali.module.financial.documentCashReceiptDocument )
*/
@Override
public void generateCoverSheet(CashReceiptDocument document, String searchPath, OutputStream returnStream) throws Exception {
if (isCoverSheetPrintingAllowed(document)) {
ByteArrayOutputStream stamperStream = new ByteArrayOutputStream();
stampPdfFormValues(document, searchPath, stamperStream);
PdfReader reader = new PdfReader(stamperStream.toByteArray());
Document pdfDoc = new Document(reader.getPageSize(FRONT_PAGE));
PdfWriter writer = PdfWriter.getInstance(pdfDoc, returnStream);
pdfDoc.open();
populateCheckDetail(document, writer, reader);
pdfDoc.close();
writer.close();
}
}
/**
* Use iText <code>{@link PdfStamper}</code> to stamp information from <code>{@link CashReceiptDocument}</code> into field
* values on a PDF Form Template.
*
* @param document The cash receipt document the values will be pulled from.
* @param searchPath The directory path of the template to be used to generate the cover sheet.
* @param returnStream The output stream the cover sheet will be written to.
*/
protected void stampPdfFormValues(CashReceiptDocument document, String searchPath, OutputStream returnStream) throws Exception {
String templateName = CR_COVERSHEET_TEMPLATE_NM;
try {
// populate form with document values
//KFSMI-7303
//The PDF template is retrieved through web static URL rather than file path, so the File separator is unnecessary
final boolean isWebResourcePath = StringUtils.containsIgnoreCase(searchPath, "HTTP");
//skip the File.separator if reference by web resource
PdfStamper stamper = new PdfStamper(new PdfReader(searchPath + (isWebResourcePath? "" : File.separator) + templateName), returnStream);
AcroFields populatedCoverSheet = stamper.getAcroFields();
populatedCoverSheet.setField(DOCUMENT_NUMBER_FIELD, document.getDocumentNumber());
populatedCoverSheet.setField(INITIATOR_FIELD, document.getDocumentHeader().getWorkflowDocument().getInitiatorPrincipalId());
populatedCoverSheet.setField(CREATED_DATE_FIELD, document.getDocumentHeader().getWorkflowDocument().getDateCreated().toString());
populatedCoverSheet.setField(AMOUNT_FIELD, document.getTotalDollarAmount().toString());
populatedCoverSheet.setField(ORG_DOC_NUMBER_FIELD, document.getDocumentHeader().getOrganizationDocumentNumber());
populatedCoverSheet.setField(CAMPUS_FIELD, document.getCampusLocationCode());
if (document.getDepositDate() != null) {
// This value won't be set until the CR document is
// deposited. A CR document is deposited only when it has
// been associated with a Cash Management Document (CMD)
// and with a Deposit within that CMD. And only when the
// CMD is submitted and FINAL, will the CR documents
// associated with it, be "deposited." So this value will
// fill in at an arbitrarily later point in time. So your
// code shouldn't expect it, but if it's there, then
// display it.
populatedCoverSheet.setField(DEPOSIT_DATE_FIELD, document.getDepositDate().toString());
}
populatedCoverSheet.setField(DESCRIPTION_FIELD, document.getDocumentHeader().getDocumentDescription());
populatedCoverSheet.setField(EXPLANATION_FIELD, document.getDocumentHeader().getExplanation());
/*
* We should print original amounts before cash manager approves the CR; after that, we should print confirmed amounts.
* Note that, in CashReceiptAction.printCoverSheet, it always retrieves the CR from DB, rather than from the current form.
* Since during CashManagement route node, the CR can't be saved until CM approves/disapproves the document; this means
* that if CM prints during this route node, he will get the original amounts. This is consistent with our logic here.
*/
boolean isConfirmed = document.isConfirmed();
KualiDecimal totalCheckAmount = !isConfirmed ? document.getTotalCheckAmount() : document.getTotalConfirmedCheckAmount();
KualiDecimal totalCurrencyAmount = !isConfirmed ? document.getTotalCurrencyAmount() : document.getTotalConfirmedCurrencyAmount();
KualiDecimal totalCoinAmount = !isConfirmed ? document.getTotalCoinAmount() : document.getTotalConfirmedCoinAmount();
KualiDecimal totalCashInAmount = !isConfirmed ? document.getTotalCashInAmount() : document.getTotalConfirmedCashInAmount();
KualiDecimal totalMoneyInAmount = !isConfirmed ? document.getTotalMoneyInAmount() : document.getTotalConfirmedMoneyInAmount();
KualiDecimal totalChangeCurrencyAmount = !isConfirmed ? document.getTotalChangeCurrencyAmount() : document.getTotalConfirmedChangeCurrencyAmount();
KualiDecimal totalChangeCoinAmount = !isConfirmed ? document.getTotalChangeCoinAmount() : document.getTotalConfirmedChangeCoinAmount();
KualiDecimal totalChangeAmount = !isConfirmed ? document.getTotalChangeAmount() : document.getTotalConfirmedChangeAmount();
KualiDecimal totalNetAmount = !isConfirmed ? document.getTotalNetAmount() : document.getTotalConfirmedNetAmount();
populatedCoverSheet.setField(CHECKS_FIELD, totalCheckAmount.toString());
populatedCoverSheet.setField(CURRENCY_FIELD, totalCurrencyAmount.toString());
populatedCoverSheet.setField(COIN_FIELD, totalCoinAmount.toString());
populatedCoverSheet.setField(CASH_IN_FIELD, totalCashInAmount.toString());
populatedCoverSheet.setField(MONEY_IN_FIELD, totalMoneyInAmount.toString());
populatedCoverSheet.setField(CHANGE_CURRENCY_FIELD, totalChangeCurrencyAmount.toString());
populatedCoverSheet.setField(CHANGE_COIN_FIELD, totalChangeCoinAmount.toString());
populatedCoverSheet.setField(CHANGE_OUT_FIELD, totalChangeAmount.toString());
populatedCoverSheet.setField(RECONCILIATION_TOTAL_FIELD, totalNetAmount.toString());
stamper.setFormFlattening(true);
stamper.close();
}
catch (Exception e) {
LOG.error("Error creating coversheet for: " + document.getDocumentNumber() + ". ::" + e);
throw e;
}
}
/**
*
* This method writes the check number from the check provided to the PDF template.
* @param output The PDF output field the check number will be written to.
* @param check The check the check number will be retrieved from.
*/
protected void writeCheckNumber(PdfContentByte output, Check check) {
writeCheckField(output, CHECK_NUMBER_FIELD_POSITION, check.getCheckNumber().toString());
}
/**
*
* This method writes the check date from the check provided to the PDF template.
* @param output The PDF output field the check date will be written to.
* @param check The check the check date will be retrieved from.
*/
protected void writeCheckDate(PdfContentByte output, Check check) {
writeCheckField(output, CHECK_DATE_FIELD_POSITION, check.getCheckDate().toString());
}
/**
*
* This method writes the check description from the check provided to the PDF template.
* @param output The PDF output field the check description will be written to.
* @param check The check the check description will be retrieved from.
*/
protected void writeCheckDescription(PdfContentByte output, Check check) {
writeCheckField(output, CHECK_DESCRIPTION_FIELD_POSITION, check.getDescription());
}
/**
*
* This method writes the check amount from the check provided to the PDF template.
* @param output The PDF output field the check amount will be written to.
* @param check The check the check amount will be retrieved from.
*/
protected void writeCheckAmount(PdfContentByte output, Check check) {
writeCheckField(output, CHECK_AMOUNT_FIELD_POSITION, check.getAmount().toString());
}
/**
*
* This method writes out the value provided to the output provided and aligns the value outputted using the xPos float
* provided.
* @param output The content byte used to write out the field to the PDF template.
* @param xPos The x coordinate of the starting point on the document where the value will be written to.
* @param fieldValue The value to be written to the PDF cover sheet.
*/
protected void writeCheckField(PdfContentByte output, float xPos, String fieldValue) {
output.beginText();
output.setTextMatrix(xPos, getCurrentRenderingYPosition());
output.newlineShowText(fieldValue);
output.endText();
}
/**
* Read-only accessor for <code>{@link BaseFont}</code>. Used for creating the check detail information. The font being
* used is Helvetica.
*
* @return A BaseFont object used to identify what type of font is used on the cover sheet.
*/
protected BaseFont getTextFont() throws DocumentException, IOException {
return BaseFont.createFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
}
/**
* Defines a state of Y position for the text.
*
* @param y The y coordinate to be set.
*/
protected void setCurrentRenderingYPosition(float y) {
_yPos = y;
}
/**
* Defines a state of Y position for the text.
*
* @return The current y coordinate.
*/
protected float getCurrentRenderingYPosition() {
return _yPos;
}
/**
* Method responsible for producing Check Detail section of the cover sheet. Not all Cash Receipt documents have checks.
*
* @param crDoc The CashReceipt document the cover sheet is being created for.
* @param writer The output writer used to write the check data to the PDF file.
* @param reader The input reader used to read data from the PDF file.
*/
protected void populateCheckDetail(CashReceiptDocument crDoc, PdfWriter writer, PdfReader reader) throws Exception {
PdfContentByte content;
ModifiableInteger pageNumber;
int checkCount = 0;
int maxCheckCount = MAX_CHECKS_FIRST_PAGE;
pageNumber = new ModifiableInteger(0);
content = startNewPage(writer, reader, pageNumber);
for (Check current : crDoc.getChecks()) {
writeCheckNumber(content, current);
writeCheckDate(content, current);
writeCheckDescription(content, current);
writeCheckAmount(content, current);
setCurrentRenderingYPosition(getCurrentRenderingYPosition() - CHECK_FIELD_HEIGHT);
checkCount++;
if (checkCount > maxCheckCount) {
checkCount = 0;
maxCheckCount = MAX_CHECKS_NORMAL;
content = startNewPage(writer, reader, pageNumber);
}
}
}
/**
* Responsible for creating a new PDF page and workspace through <code>{@link PdfContentByte}</code> for direct writing to the
* PDF.
*
* @param writer The PDF writer used to write to the new page with.
* @param reader The PDF reader used to read information from the PDF file.
* @param pageNumber The current number of pages in the PDF file, which will be incremented by one inside this method.
*
* @return The PDFContentByte used to access the new PDF page.
* @exception DocumentException
* @exception IOException
*/
protected PdfContentByte startNewPage(PdfWriter writer, PdfReader reader, ModifiableInteger pageNumber) throws DocumentException, IOException {
PdfContentByte retval;
PdfContentByte under;
Rectangle pageSize;
Document pdfDoc;
PdfImportedPage newPage;
pageNumber.increment();
pageSize = reader.getPageSize(FRONT_PAGE);
retval = writer.getDirectContent();
// under = writer.getDirectContentUnder();
if (pageNumber.getInt() > FRONT_PAGE) {
newPage = writer.getImportedPage(reader, CHECK_PAGE_NORMAL);
setCurrentRenderingYPosition(pageSize.top(TOP_MARGIN + CHECK_DETAIL_HEADING_HEIGHT));
}
else {
newPage = writer.getImportedPage(reader, FRONT_PAGE);
setCurrentRenderingYPosition(pageSize.top(TOP_FIRST_PAGE));
}
pdfDoc = retval.getPdfDocument();
pdfDoc.newPage();
retval.addTemplate(newPage, 0, 0);
retval.setFontAndSize(getTextFont(), 8);
return retval;
}
/**
* Gets the dataDictionaryService attribute.
* @return Returns the dataDictionaryService.
*/
public DataDictionaryService getDataDictionaryService() {
return dataDictionaryService;
}
/**
* Sets the dataDictionaryService attribute value.
* @param dataDictionaryService The dataDictionaryService to set.
*/
public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
this.dataDictionaryService = dataDictionaryService;
}
/**
* Gets the documentHelperService attribute.
* @return Returns the documentHelperService.
*/
public DocumentHelperService getDocumentHelperService() {
return documentHelperService;
}
/**
* Sets the documentHelperService attribute value.
* @param documentHelperService The documentHelperService to set.
*/
public void setDocumentHelperService(DocumentHelperService documentHelperService) {
this.documentHelperService = documentHelperService;
}
}
/**
* Utility class used to replace an <code>{@link Integer}</code> because an integer cannot be modified once it has been
* instantiated.
*/
class ModifiableInteger {
int _value;
/**
*
* Constructs a ModifiableInteger object.
* @param val The initial value of the object.
*/
public ModifiableInteger(Integer val) {
this(val.intValue());
}
/**
*
* Constructs a ModifiableInteger object.
* @param val The initial value of the object.
*/
public ModifiableInteger(int val) {
setInt(val);
}
/**
*
* This method sets the local attribute to the value given.
* @param val The int value to be set.
*/
public void setInt(int val) {
_value = val;
}
/**
*
* This method retrieves the value of the object.
* @return The int value of this object.
*/
public int getInt() {
return _value;
}
/**
*
* This method increments the value of this class by one.
* @return An instance of this class with the value incremented by one.
*/
public ModifiableInteger increment() {
_value++;
return this;
}
/**
*
* This method increments the value of this class by the amount specified.
* @param inc The amount the class value should be incremented by.
* @return An instance of this class with the value incremented by the amount specified.
*/
public ModifiableInteger increment(int inc) {
_value += inc;
return this;
}
/**
*
* This method decrements the value of this class by one.
* @return An instance of this class with the value decremented by one.
*/
public ModifiableInteger decrement() {
_value--;
return this;
}
/**
*
* This method decrements the value of this class by the amount specified.
* @param dec The amount the class value should be decremented by.
* @return An instance of this class with the value decremented by the amount specified.
*/
public ModifiableInteger decrement(int dec) {
_value -= dec;
return this;
}
/**
*
* This method converts the value of this class and returns it as an Integer object.
* @return The value of this class formatted as an Integer.
*/
public Integer getInteger() {
return new Integer(_value);
}
/**
* This method generates and returns a String representation of this class.
* @return A string representation of this object.
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return getInteger().toString();
}
}