/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 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.module.ar.document;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.Chart;
import org.kuali.kfs.coa.businessobject.Organization;
import org.kuali.kfs.integration.ar.AccountsReceivableCustomerAddress;
import org.kuali.kfs.integration.ar.AccountsReceivableCustomerInvoice;
import org.kuali.kfs.integration.ar.AccountsReceivableCustomerInvoiceRecurrenceDetails;
import org.kuali.kfs.kns.document.MaintenanceDocument;
import org.kuali.kfs.krad.UserSession;
import org.kuali.kfs.krad.bo.DocumentHeader;
import org.kuali.kfs.krad.document.Copyable;
import org.kuali.kfs.krad.service.DocumentService;
import org.kuali.kfs.krad.util.GlobalVariables;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.ar.ArKeyConstants;
import org.kuali.kfs.module.ar.businessobject.AccountsReceivableDocumentHeader;
import org.kuali.kfs.module.ar.businessobject.Customer;
import org.kuali.kfs.module.ar.businessobject.CustomerAddress;
import org.kuali.kfs.module.ar.businessobject.CustomerInvoiceDetail;
import org.kuali.kfs.module.ar.businessobject.CustomerInvoiceRecurrenceDetails;
import org.kuali.kfs.module.ar.businessobject.CustomerProcessingType;
import org.kuali.kfs.module.ar.businessobject.InvoiceRecurrence;
import org.kuali.kfs.module.ar.businessobject.PrintInvoiceOptions;
import org.kuali.kfs.module.ar.businessobject.ReceivableCustomerInvoiceDetail;
import org.kuali.kfs.module.ar.businessobject.SalesTaxCustomerInvoiceDetail;
import org.kuali.kfs.module.ar.document.service.AccountsReceivablePendingEntryService;
import org.kuali.kfs.module.ar.document.service.AccountsReceivableTaxService;
import org.kuali.kfs.module.ar.document.service.CustomerAddressService;
import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDetailService;
import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDocumentService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail;
import org.kuali.kfs.sys.businessobject.TaxDetail;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.AccountingDocumentBase;
import org.kuali.kfs.sys.document.AmountTotaling;
import org.kuali.kfs.sys.document.Correctable;
import org.kuali.kfs.sys.document.dataaccess.FinancialSystemDocumentHeaderDao;
import org.kuali.kfs.sys.service.GeneralLedgerPendingEntryService;
import org.kuali.kfs.sys.service.TaxService;
import org.kuali.kfs.sys.util.KfsDateUtils;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;

import java.sql.Date;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;

public class CustomerInvoiceDocument extends AccountingDocumentBase implements AmountTotaling, Copyable, Correctable, Comparable<CustomerInvoiceDocument>, AccountsReceivableCustomerInvoice {
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CustomerInvoiceDocument.class);

    protected static final String HAS_RECCURENCE_NODE = "HasReccurence";
    protected static final String BATCH_GENERATED_NODE = "BatchGenerated";

    protected String invoiceHeaderText;
    protected String invoiceAttentionLineText;
    protected Date invoiceDueDate;
    protected Date billingDate;
    protected Date closedDate;
    protected Date billingDateForDisplay;
    protected String invoiceTermsText;
    protected String organizationInvoiceNumber;
    protected String customerPurchaseOrderNumber;
    protected String printInvoiceIndicator;
    protected Date customerPurchaseOrderDate;
    protected String billByChartOfAccountCode;
    protected String billedByOrganizationCode;
    protected Integer customerShipToAddressIdentifier;
    protected Integer customerBillToAddressIdentifier;
    protected String customerSpecialProcessingCode;
    protected boolean customerRecordAttachmentIndicator;
    protected boolean openInvoiceIndicator;
    protected Date printDate;
    protected Integer age;
    protected String customerName;
    protected String billingAddressName;
    protected String billingCityName;
    protected String billingStateCode;
    protected String billingZipCode;
    protected String billingCountryCode;
    protected String billingAddressInternationalProvinceName;
    protected String billingInternationalMailCode;
    protected String billingEmailAddress;
    protected String billingAddressTypeCode;
    protected String billingLine1StreetAddress;
    protected String billingLine2StreetAddress;
    protected String shippingLine1StreetAddress;
    protected String shippingLine2StreetAddress;
    protected String shippingAddressName;
    protected String shippingCityName;
    protected String shippingStateCode;
    protected String shippingZipCode;
    protected String shippingCountryCode;
    protected String shippingAddressInternationalProvinceName;
    protected String shippingInternationalMailCode;
    protected String shippingEmailAddress;
    protected String shippingAddressTypeCode;
    protected boolean recurredInvoiceIndicator;
    protected Date reportedDate;

    protected AccountsReceivableDocumentHeader accountsReceivableDocumentHeader;
    protected Chart billByChartOfAccount;
    protected Organization billedByOrganization;
    protected CustomerProcessingType customerSpecialProcessing;
    protected PrintInvoiceOptions printInvoiceOption;
    protected CustomerAddress customerShipToAddress;
    protected CustomerAddress customerBillToAddress;
    protected CustomerInvoiceRecurrenceDetails customerInvoiceRecurrenceDetails;

    /**
     * Default constructor.
     */
    public CustomerInvoiceDocument() {
        super();
    }

    /**
     * This method calculates the outstanding balance on an invoice.
     *
     * @return the outstanding balance on this invoice
     */
    @Override
    public KualiDecimal getOpenAmount() {
        return SpringContext.getBean(CustomerInvoiceDocumentService.class).getOpenAmountForCustomerInvoiceDocument(this);
    }

    /**
     * Gets the documentNumber attribute.
     *
     * @return Returns the documentNumber
     */
    @Override
    public String getDocumentNumber() {
        return documentNumber;
    }

    /**
     * Sets the documentNumber attribute.
     *
     * @param documentNumber The documentNumber to set.
     */
    @Override
    public void setDocumentNumber(String documentNumber) {
        this.documentNumber = documentNumber;
    }


    /**
     * Gets the invoiceHeaderText attribute.
     *
     * @return Returns the invoiceHeaderText
     */
    public String getInvoiceHeaderText() {
        return invoiceHeaderText;
    }

    /**
     * Sets the invoiceHeaderText attribute.
     *
     * @param invoiceHeaderText The invoiceHeaderText to set.
     */
    public void setInvoiceHeaderText(String invoiceHeaderText) {
        this.invoiceHeaderText = invoiceHeaderText;
    }


    /**
     * Gets the invoiceAttentionLineText attribute.
     *
     * @return Returns the invoiceAttentionLineText
     */
    @Override
    public String getInvoiceAttentionLineText() {
        return invoiceAttentionLineText;
    }

    /**
     * Sets the invoiceAttentionLineText attribute.
     *
     * @param invoiceAttentionLineText The invoiceAttentionLineText to set.
     */
    public void setInvoiceAttentionLineText(String invoiceAttentionLineText) {
        this.invoiceAttentionLineText = invoiceAttentionLineText;
    }


    /**
     * Gets the invoiceDueDate attribute.
     *
     * @return Returns the invoiceDueDate
     */
    @Override
    public Date getInvoiceDueDate() {
        return invoiceDueDate;
    }

    /**
     * Sets the invoiceDueDate attribute.
     *
     * @param invoiceDueDate The invoiceDueDate to set.
     */
    @Override
    public void setInvoiceDueDate(Date invoiceDueDate) {
        this.invoiceDueDate = invoiceDueDate;
    }


    /**
     * Gets the billingDate attribute.
     *
     * @return Returns the billingDate
     */
    @Override
    public Date getBillingDate() {
        return billingDate;
    }

    /**
     * This method returns the age of an invoice (i.e. current date - billing date)
     *
     * @return
     */
    @Override
    public Integer getAge() {
        if (ObjectUtils.isNotNull(billingDate)) {
            return (int) KfsDateUtils.getDifferenceInDays(new Timestamp(billingDate.getTime()), SpringContext.getBean(DateTimeService.class).getCurrentTimestamp());
        }
        // TODO should I be throwing an exception or throwing a null?
        return null;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    /**
     * Sets the billingDate attribute.
     *
     * @param billingDate The billingDate to set.
     */
    @Override
    public void setBillingDate(Date billingDate) {
        this.billingDate = billingDate;
    }


    /**
     * Gets the invoiceTermsText attribute.
     *
     * @return Returns the invoiceTermsText
     */
    @Override
    public String getInvoiceTermsText() {
        return invoiceTermsText;
    }

    /**
     * Sets the invoiceTermsText attribute.
     *
     * @param invoiceTermsText The invoiceTermsText to set.
     */
    @Override
    public void setInvoiceTermsText(String invoiceTermsText) {
        this.invoiceTermsText = invoiceTermsText;
    }


    /**
     * Gets the organizationInvoiceNumber attribute.
     *
     * @return Returns the organizationInvoiceNumber
     */
    public String getOrganizationInvoiceNumber() {
        return organizationInvoiceNumber;
    }

    /**
     * Sets the organizationInvoiceNumber attribute.
     *
     * @param organizationInvoiceNumber The organizationInvoiceNumber to set.
     */
    @Override
    public void setOrganizationInvoiceNumber(String organizationInvoiceNumber) {
        this.organizationInvoiceNumber = organizationInvoiceNumber;
    }

    /**
     * Gets the customerPurchaseOrderNumber attribute.
     *
     * @return Returns the customerPurchaseOrderNumber
     */
    public String getCustomerPurchaseOrderNumber() {
        return customerPurchaseOrderNumber;
    }

    /**
     * Sets the customerPurchaseOrderNumber attribute.
     *
     * @param customerPurchaseOrderNumber The customerPurchaseOrderNumber to set.
     */
    public void setCustomerPurchaseOrderNumber(String customerPurchaseOrderNumber) {
        this.customerPurchaseOrderNumber = customerPurchaseOrderNumber;
    }

    /**
     * Gets the printInvoiceIndicator attribute.
     *
     * @return Returns the printInvoiceIndicator.
     */
    public String getPrintInvoiceIndicator() {
        return printInvoiceIndicator;
    }

    /**
     * Sets the printInvoiceIndicator attribute value.
     *
     * @param printInvoiceIndicator The printInvoiceIndicator to set.
     */
    @Override
    public void setPrintInvoiceIndicator(String printInvoiceIndicator) {
        this.printInvoiceIndicator = printInvoiceIndicator;
    }

    /**
     * Gets the customerPurchaseOrderDate attribute.
     *
     * @return Returns the customerPurchaseOrderDate
     */
    public Date getCustomerPurchaseOrderDate() {
        return customerPurchaseOrderDate;
    }

    /**
     * Sets the customerPurchaseOrderDate attribute.
     *
     * @param customerPurchaseOrderDate The customerPurchaseOrderDate to set.
     */
    public void setCustomerPurchaseOrderDate(Date customerPurchaseOrderDate) {
        this.customerPurchaseOrderDate = customerPurchaseOrderDate;
    }


    /**
     * Gets the billByChartOfAccountCode attribute.
     *
     * @return Returns the billByChartOfAccountCode
     */
    @Override
    public String getBillByChartOfAccountCode() {
        return billByChartOfAccountCode;
    }

    /**
     * Sets the billByChartOfAccountCode attribute.
     *
     * @param billByChartOfAccountCode The billByChartOfAccountCode to set.
     */
    @Override
    public void setBillByChartOfAccountCode(String billByChartOfAccountCode) {
        this.billByChartOfAccountCode = billByChartOfAccountCode;
    }

    /**
     * Gets the billedByOrganizationCode attribute.
     *
     * @return Returns the billedByOrganizationCode
     */
    @Override
    public String getBilledByOrganizationCode() {
        return billedByOrganizationCode;
    }

    /**
     * Sets the billedByOrganizationCode attribute.
     *
     * @param billedByOrganizationCode The billedByOrganizationCode to set.
     */
    @Override
    public void setBilledByOrganizationCode(String billedByOrganizationCode) {
        this.billedByOrganizationCode = billedByOrganizationCode;
    }

    /**
     * Gets the customerShipToAddressIdentifier attribute.
     *
     * @return Returns the customerShipToAddressIdentifier
     */
    public Integer getCustomerShipToAddressIdentifier() {
        return customerShipToAddressIdentifier;
    }

    /**
     * Sets the customerShipToAddressIdentifier attribute.
     *
     * @param customerShipToAddressIdentifier The customerShipToAddressIdentifier to set.
     */
    public void setCustomerShipToAddressIdentifier(Integer customerShipToAddressIdentifier) {
        this.customerShipToAddressIdentifier = customerShipToAddressIdentifier;
    }


    /**
     * Gets the customerBillToAddressIdentifier attribute.
     *
     * @return Returns the customerBillToAddressIdentifier
     */
    public Integer getCustomerBillToAddressIdentifier() {
        return customerBillToAddressIdentifier;
    }

    /**
     * Sets the customerBillToAddressIdentifier attribute.
     *
     * @param customerBillToAddressIdentifier The customerBillToAddressIdentifier to set.
     */
    @Override
    public void setCustomerBillToAddressIdentifier(Integer customerBillToAddressIdentifier) {
        this.customerBillToAddressIdentifier = customerBillToAddressIdentifier;
    }


    /**
     * Gets the customerSpecialProcessingCode attribute.
     *
     * @return Returns the customerSpecialProcessingCode
     */
    public String getCustomerSpecialProcessingCode() {
        return customerSpecialProcessingCode;
    }

    /**
     * Sets the customerSpecialProcessingCode attribute.
     *
     * @param customerSpecialProcessingCode The customerSpecialProcessingCode to set.
     */
    public void setCustomerSpecialProcessingCode(String customerSpecialProcessingCode) {
        this.customerSpecialProcessingCode = customerSpecialProcessingCode;
    }


    /**
     * Gets the customerRecordAttachmentIndicator attribute.
     *
     * @return Returns the customerRecordAttachmentIndicator
     */
    public boolean isCustomerRecordAttachmentIndicator() {
        return customerRecordAttachmentIndicator;
    }

    /**
     * Sets the customerRecordAttachmentIndicator attribute.
     *
     * @param customerRecordAttachmentIndicator The customerRecordAttachmentIndicator to set.
     */
    public void setCustomerRecordAttachmentIndicator(boolean customerRecordAttachmentIndicator) {
        this.customerRecordAttachmentIndicator = customerRecordAttachmentIndicator;
    }


    /**
     * Gets the openInvoiceIndicator attribute.
     *
     * @return Returns the openInvoiceIndicator
     */
    @Override
    public boolean isOpenInvoiceIndicator() {
        return openInvoiceIndicator;
    }

    /**
     * Sets the openInvoiceIndicator attribute.
     *
     * @param openInvoiceIndicator The openInvoiceIndicator to set.
     */
    @Override
    public void setOpenInvoiceIndicator(boolean openInvoiceIndicator) {
        this.openInvoiceIndicator = openInvoiceIndicator;
    }

    /**
     * Gets the printDate attribute.
     *
     * @return Returns the printDate
     */
    public Date getPrintDate() {
        return printDate;
    }

    /**
     * Sets the printDate attribute.
     *
     * @param printDate The printDate to set.
     */
    public void setPrintDate(Date printDate) {
        this.printDate = printDate;
    }

    /**
     * Gets the accountsReceivableDocumentHeader attribute.
     *
     * @return Returns the accountsReceivableDocumentHeader
     */
    @Override
    public AccountsReceivableDocumentHeader getAccountsReceivableDocumentHeader() {
        return accountsReceivableDocumentHeader;
    }

    /**
     * Sets the accountsReceivableDocumentHeader attribute.
     *
     * @param accountsReceivableDocumentHeader The accountsReceivableDocumentHeader to set.
     */
    public void setAccountsReceivableDocumentHeader(AccountsReceivableDocumentHeader accountsReceivableDocumentHeader) {
        this.accountsReceivableDocumentHeader = accountsReceivableDocumentHeader;
    }

    /**
     * This method...
     *
     * @return
     */
    public String getParentInvoiceNumber() {
        return getAccountsReceivableDocumentHeader().getDocumentHeader().getDocumentTemplateNumber();
    }

    /**
     * Gets the billByChartOfAccount attribute.
     *
     * @return Returns the billByChartOfAccount
     */
    public Chart getBillByChartOfAccount() {
        return billByChartOfAccount;
    }

    /**
     * Sets the billByChartOfAccount attribute.
     *
     * @param billByChartOfAccount The billByChartOfAccount to set.
     * @deprecated
     */
    @Deprecated
    public void setBillByChartOfAccount(Chart billByChartOfAccount) {
        this.billByChartOfAccount = billByChartOfAccount;
    }

    /**
     * Gets the billedByOrganization attribute.
     *
     * @return Returns the billedByOrganization
     */
    public Organization getBilledByOrganization() {
        return billedByOrganization;
    }

    /**
     * Sets the billedByOrganization attribute.
     *
     * @param billedByOrganization The billedByOrganization to set.
     * @deprecated
     */
    @Deprecated
    public void setBilledByOrganization(Organization billedByOrganization) {
        this.billedByOrganization = billedByOrganization;
    }

    /**
     * Gets the customerSpecialProcessing attribute.
     *
     * @return Returns the customerSpecialProcessing
     */
    public CustomerProcessingType getCustomerSpecialProcessing() {
        return customerSpecialProcessing;
    }

    /**
     * Sets the customerSpecialProcessing attribute.
     *
     * @param customerSpecialProcessing The customerSpecialProcessing to set.
     * @deprecated
     */
    @Deprecated
    public void setCustomerSpecialProcessing(CustomerProcessingType customerSpecialProcessing) {
        this.customerSpecialProcessing = customerSpecialProcessing;
    }

    /**
     * This method returns the billing date for display. If billing date hasn't been set yet, just display current date
     *
     * @return
     */
    public Date getBillingDateForDisplay() {
        if (ObjectUtils.isNotNull(getBillingDate())) {
            return getBillingDate();
        } else {
            return SpringContext.getBean(DateTimeService.class).getCurrentSqlDate();
        }
    }

    /**
     * This method...
     *
     * @param date
     */
    public void setBillingDateForDisplay(Date date) {
        // do nothing
    }

    public Date getClosedDate() {
        return closedDate;
    }

    public void setClosedDate(Date closedDate) {
        this.closedDate = closedDate;
    }

    /**
     * @see org.kuali.rice.krad.bo.BusinessObjectBase#toStringMapper()
     */
    @SuppressWarnings("unchecked")
    protected LinkedHashMap toStringMapper_RICE20_REFACTORME() {
        LinkedHashMap m = new LinkedHashMap();
        m.put("documentNumber", this.documentNumber);
        return m;
    }

    /**
     * This method returns true if this document has been corrected
     *
     * @return
     */
    public boolean hasInvoiceBeenCorrected() {
        DocumentHeader documentHeader = SpringContext.getBean(FinancialSystemDocumentHeaderDao.class).getCorrectingDocumentHeader(documentNumber);

        if (ObjectUtils.isNotNull(documentHeader) && StringUtils.isNotBlank(documentHeader.getDocumentNumber())) {
            return true;
        }

        return false;
    }

    /**
     * This method returns true if this document is a reversal for another document
     *
     * @return
     */
    public boolean isInvoiceReversal() {
        return ObjectUtils.isNotNull(getFinancialSystemDocumentHeader().getFinancialDocumentInErrorNumber());
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocumentBase#isDebit(org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail)
     */
    @Override
    public boolean isDebit(GeneralLedgerPendingEntrySourceDetail postable) {
        return ((CustomerInvoiceDetail) postable).isDebit();
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocumentBase#getSourceAccountingLineClass()
     */
    @Override
    public Class<CustomerInvoiceDetail> getSourceAccountingLineClass() {
        return CustomerInvoiceDetail.class;
    }

    /**
     * Ensures that all the accounts receivable object codes are correctly updated
     */
    public void updateAccountReceivableObjectCodes() {
        for (Iterator e = getSourceAccountingLines().iterator(); e.hasNext(); ) {
            SpringContext.getBean(CustomerInvoiceDetailService.class).updateAccountsReceivableObjectCode(((CustomerInvoiceDetail) e.next()));
        }
    }

    /**
     * This method creates the following GLPE's for the invoice 1. Debit to receivable for total line amount ( including sales tax
     * if it exists ). 2. Credit to income based on item price * quantity. 3. Credit to state sales tax account/object code if state
     * sales tax exists. 4. Credit to district sales tax account/object code if district sales tax exists.
     *
     * @see org.kuali.kfs.service.impl.GenericGeneralLedgerPendingEntryGenerationProcessImpl#processGenerateGeneralLedgerPendingEntries(org.kuali.kfs.sys.document.GeneralLedgerPendingEntrySource,
     * org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail,
     * org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper)
     */
    @Override
    public boolean generateGeneralLedgerPendingEntries(GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, GeneralLedgerPendingEntrySequenceHelper sequenceHelper) {
        addReceivableGLPEs(sequenceHelper, glpeSourceDetail);
        sequenceHelper.increment();
        addIncomeGLPEs(sequenceHelper, glpeSourceDetail);

        // if sales tax is enabled generate GLPEs
        if (SpringContext.getBean(AccountsReceivableTaxService.class).isCustomerInvoiceDetailTaxable(this, (CustomerInvoiceDetail) glpeSourceDetail)) {
            addSalesTaxGLPEs(sequenceHelper, glpeSourceDetail);
        }


        return true;
    }


    /**
     * This method creates the receivable GLPEs for each invoice detail line.
     *
     * @param poster
     * @param sequenceHelper
     * @param postable
     * @param explicitEntry
     */
    protected void addReceivableGLPEs(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail) {
        CustomerInvoiceDetail customerInvoiceDetail = (CustomerInvoiceDetail) glpeSourceDetail;
        ReceivableCustomerInvoiceDetail receivableCustomerInvoiceDetail = new ReceivableCustomerInvoiceDetail(customerInvoiceDetail, this);

        AccountsReceivablePendingEntryService service = SpringContext.getBean(AccountsReceivablePendingEntryService.class);
        service.createAndAddGenericInvoiceRelatedGLPEs(this, receivableCustomerInvoiceDetail, sequenceHelper, isInvoiceDetailReceivableDebit(customerInvoiceDetail), false, customerInvoiceDetail.getInvoiceItemPreTaxAmount());
    }

    /**
     * Determines if the given customer invoice detail should be a debit on the receivable glpe, or a credit
     *
     * @param customerInvoiceDetail the customer invoice detail to determine debitness of
     * @return true if the detail represents a debit, false if it represents a credit
     */
    protected boolean isInvoiceDetailReceivableDebit(CustomerInvoiceDetail customerInvoiceDetail) {
        final boolean isDebit = (!isInvoiceReversal() && !customerInvoiceDetail.isDiscountLine()) || (isInvoiceReversal() && customerInvoiceDetail.isDiscountLine());
        return isDebit;
    }

    /**
     * This method adds pending entry with transaction ledger entry amount set to item price * quantity
     *
     * @param poster
     * @param sequenceHelper
     * @param postable
     * @param explicitEntry
     */
    protected void addIncomeGLPEs(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail) {

        CustomerInvoiceDetail customerInvoiceDetail = (CustomerInvoiceDetail) glpeSourceDetail;

        AccountsReceivablePendingEntryService service = SpringContext.getBean(AccountsReceivablePendingEntryService.class);
        service.createAndAddGenericInvoiceRelatedGLPEs(this, customerInvoiceDetail, sequenceHelper, isInvoiceDetailIncomeDebit(customerInvoiceDetail), false, customerInvoiceDetail.getInvoiceItemPreTaxAmount());
    }

    /**
     * Determines if the given customer invoice detail should be a debit on the income glpe, or a credit
     *
     * @param customerInvoiceDetail the customer invoice detail to determine debitness of
     * @return true if the detail represents a debit, false if it represents a credit
     */
    protected boolean isInvoiceDetailIncomeDebit(CustomerInvoiceDetail customerInvoiceDetail) {
        final boolean isDebit = (!isInvoiceReversal() && customerInvoiceDetail.isDiscountLine()) || (isInvoiceReversal() && !customerInvoiceDetail.isDiscountLine());
        return isDebit;
    }

    /**
     * This method add pending entries for every tax detail that exists for a particular postal code
     *
     * @param sequenceHelper
     * @param glpeSourceDetail
     * @param hasClaimOnCashOffset
     */
    protected void addSalesTaxGLPEs(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail) {

        CustomerInvoiceDetail customerInvoiceDetail = (CustomerInvoiceDetail) glpeSourceDetail;
        boolean isDebit = (!isInvoiceReversal() && customerInvoiceDetail.isDiscountLine()) || (isInvoiceReversal() && !customerInvoiceDetail.isDiscountLine());

        String postalCode = SpringContext.getBean(AccountsReceivableTaxService.class).getPostalCodeForTaxation(this);
        Date dateOfTransaction = SpringContext.getBean(DateTimeService.class).getCurrentSqlDate();

        List<TaxDetail> salesTaxDetails = SpringContext.getBean(TaxService.class).getSalesTaxDetails(dateOfTransaction, postalCode, customerInvoiceDetail.getInvoiceItemPreTaxAmount());

        AccountsReceivablePendingEntryService service = SpringContext.getBean(AccountsReceivablePendingEntryService.class);
        SalesTaxCustomerInvoiceDetail salesTaxCustomerInvoiceDetail;
        ReceivableCustomerInvoiceDetail receivableCustomerInvoiceDetail;
        for (TaxDetail salesTaxDetail : salesTaxDetails) {

            salesTaxCustomerInvoiceDetail = new SalesTaxCustomerInvoiceDetail(salesTaxDetail, customerInvoiceDetail);
            receivableCustomerInvoiceDetail = new ReceivableCustomerInvoiceDetail(salesTaxCustomerInvoiceDetail, this);

            sequenceHelper.increment();
            service.createAndAddGenericInvoiceRelatedGLPEs(this, receivableCustomerInvoiceDetail, sequenceHelper, !isDebit, false, salesTaxDetail.getTaxAmount());

            sequenceHelper.increment();
            service.createAndAddGenericInvoiceRelatedGLPEs(this, salesTaxCustomerInvoiceDetail, sequenceHelper, isDebit, false, salesTaxDetail.getTaxAmount());
        }
    }

    /**
     * Returns an implementation of the GeneralLedgerPendingEntryService
     *
     * @return an implementation of the GeneralLedgerPendingEntryService
     */
    public GeneralLedgerPendingEntryService getGeneralLedgerPendingEntryService() {
        return SpringContext.getBean(GeneralLedgerPendingEntryService.class);
    }

    @Override
    public List<String> getWorkflowEngineDocumentIdsToLock() {
        //  add the invoice number of the Error Corrected doc, if this is an error correction
        if (this.isInvoiceReversal()) {
            if (StringUtils.isNotBlank(getFinancialSystemDocumentHeader().getFinancialDocumentInErrorNumber())) {
                List<String> documentIds = new ArrayList<String>();
                documentIds.add(getFinancialSystemDocumentHeader().getFinancialDocumentInErrorNumber());
                return documentIds;
            }
        }
        return null;
    }

    /**
     * When document is processed do the following: 1) Set the billingDate to today's date if not already set 2) If there are
     * discounts, create corresponding invoice paid applied rows 3) If the document is a reversal, in addition to reversing paid
     * applied rows, update the open paid applied indicator
     *
     * @see org.kuali.kfs.sys.document.GeneralLedgerPostingDocumentBase#doRouteStatusChange()
     */
    @Override
    public void doRouteStatusChange(DocumentRouteStatusChange statusChangeEvent) {
        super.doRouteStatusChange(statusChangeEvent);

        //  fast-exit if status != P
        if (!getDocumentHeader().getWorkflowDocument().isProcessed()) {
            return;
        }

        //  wire up the billing date
        if (ObjectUtils.isNull(getBillingDate())) {
            setBillingDate(SpringContext.getBean(DateTimeService.class).getCurrentSqlDateMidnight());
        }

        // apply discounts
        CustomerInvoiceDocumentService invoiceService = SpringContext.getBean(CustomerInvoiceDocumentService.class);
        invoiceService.convertDiscountsToPaidApplieds(this);

        //  handle a Correction/Reversal document
        if (this.isInvoiceReversal()) {
            CustomerInvoiceDocument correctedCustomerInvoiceDocument;
            try {
                correctedCustomerInvoiceDocument = (CustomerInvoiceDocument) SpringContext.getBean(DocumentService.class).getByDocumentHeaderId(this.getFinancialSystemDocumentHeader().getFinancialDocumentInErrorNumber());
            } catch (WorkflowException e) {
                throw new RuntimeException("Cannot find customer invoice document with id " + this.getFinancialSystemDocumentHeader().getFinancialDocumentInErrorNumber());
            }

            // if reversal, close both this reversal invoice and the original invoice
            SpringContext.getBean(CustomerInvoiceDocumentService.class).closeCustomerInvoiceDocument(correctedCustomerInvoiceDocument);
            SpringContext.getBean(CustomerInvoiceDocumentService.class).closeCustomerInvoiceDocument(this);
        }

        //  handle Recurrence
        if (ObjectUtils.isNull(this.getCustomerInvoiceRecurrenceDetails()) ||
            (ObjectUtils.isNull(this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceBeginDate())
                && ObjectUtils.isNull(this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceEndDate())
                && ObjectUtils.isNull(this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceIntervalCode())
                && ObjectUtils.isNull(this.getCustomerInvoiceRecurrenceDetails().getDocumentTotalRecurrenceNumber())
                && ObjectUtils.isNull(this.getCustomerInvoiceRecurrenceDetails().getDocumentInitiatorUserIdentifier()))) {
        } else {
            // set new user session to recurrence initiator
            UserSession currentSession = GlobalVariables.getUserSession();
            GlobalVariables.setUserSession(new UserSession(KFSConstants.SYSTEM_USER));

            // populate InvoiceRecurrence business object
            InvoiceRecurrence newInvoiceRecurrence = new InvoiceRecurrence();
            newInvoiceRecurrence.setInvoiceNumber(this.getCustomerInvoiceRecurrenceDetails().getInvoiceNumber());
            newInvoiceRecurrence.setCustomerNumber(this.getCustomerInvoiceRecurrenceDetails().getCustomerNumber());
            newInvoiceRecurrence.setDocumentRecurrenceBeginDate(this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceBeginDate());
            newInvoiceRecurrence.setDocumentRecurrenceEndDate(this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceEndDate());
            newInvoiceRecurrence.setDocumentRecurrenceIntervalCode(this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceIntervalCode());
            newInvoiceRecurrence.setDocumentTotalRecurrenceNumber(this.getCustomerInvoiceRecurrenceDetails().getDocumentTotalRecurrenceNumber());
            newInvoiceRecurrence.setDocumentInitiatorUserIdentifier(this.getCustomerInvoiceRecurrenceDetails().getDocumentInitiatorUserIdentifier());
            newInvoiceRecurrence.setActive(this.getCustomerInvoiceRecurrenceDetails().isActive());

            // create a new InvoiceRecurrenceMaintenanceDocument
            MaintenanceDocument invoiceRecurrenceMaintDoc = null;
            try {
                invoiceRecurrenceMaintDoc = (MaintenanceDocument) SpringContext.getBean(DocumentService.class).getNewDocument(getInvoiceRecurrenceMaintenanceDocumentTypeName());
            } catch (WorkflowException e1) {
                throw new RuntimeException("Cannot create new Invoice Recurrence Maintenance Document.");
            }
            invoiceRecurrenceMaintDoc.getDocumentHeader().setDocumentDescription("Automatically created from Invoice");
            invoiceRecurrenceMaintDoc.getNewMaintainableObject().setBusinessObject(newInvoiceRecurrence);

            try {
                // blanket approve the INVR, bypassing everything
                //invoiceRecurrenceMaintDoc.getDocumentHeader().getWorkflowDocument().blanketApprove("Blanket Approved by the creating Invoice Document #" + getDocumentNumber());
                //TODO temporarily just do regular route until we can get blanket approve perms setup for KFS
                SpringContext.getBean(DocumentService.class).saveDocument(invoiceRecurrenceMaintDoc);
                invoiceRecurrenceMaintDoc.getDocumentHeader().getWorkflowDocument().route("Automatically created and routed by CustomerInvoiceDocument #" + getDocumentNumber() + ".");
            } catch (WorkflowException e) {
                throw new RuntimeException("Cannot route Invoice Recurrence Maintenance Document with id " + invoiceRecurrenceMaintDoc.getDocumentNumber() + ".");
            }

            // return the session to the original initiator
            GlobalVariables.setUserSession(currentSession);

        }
    }

    protected String getInvoiceRecurrenceMaintenanceDocumentTypeName() {
        return "INVR";
    }

    /**
     * If this invoice is a reversal, set the open indicator to false
     *
     * @see org.kuali.kfs.sys.document.FinancialSystemTransactionalDocumentBase#prepareForSave()
     */
    @Override
    public void prepareForSave() {
        if (this.isInvoiceReversal()) {
            setOpenInvoiceIndicator(false);
        }

        //  make sure the docHeader gets its doc total right.  This is here because there's an ordering
        // bug in the struts classes for invoice that is preventing this from being set right.  There is
        // probably a better way to fix this that can be pursued later.
        getFinancialSystemDocumentHeader().setFinancialDocumentTotalAmount(getTotalDollarAmount());

        captureWorkflowHeaderInformation();

        //  invoice recurrence stuff, if there is a recurrence object
        if (ObjectUtils.isNotNull(this.getCustomerInvoiceRecurrenceDetails()) && getProcessRecurrenceFlag()) {

            //  wire up the recurrence customer number if one exists
            if (ObjectUtils.isNull(this.getCustomerInvoiceRecurrenceDetails().getCustomerNumber())) {
                this.getCustomerInvoiceRecurrenceDetails().setCustomerNumber(this.getAccountsReceivableDocumentHeader().getCustomerNumber());
            }

            customerInvoiceRecurrenceDetails.setInvoiceNumber(getDocumentNumber());

            // calc recurrence number if only end-date specified
            if (ObjectUtils.isNull(this.getCustomerInvoiceRecurrenceDetails().getDocumentTotalRecurrenceNumber()) && ObjectUtils.isNotNull(this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceEndDate())) {

                Calendar beginCalendar = Calendar.getInstance();
                beginCalendar.setTime(this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceBeginDate());
                Date beginDate = this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceBeginDate();
                Calendar endCalendar = Calendar.getInstance();

                endCalendar.setTime(this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceEndDate());
                Date endDate = this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceEndDate();
                Calendar nextCalendar = Calendar.getInstance();
                Date nextDate = beginDate;

                int totalRecurrences = 0;
                int addCounter = 0;
                String intervalCode = this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceIntervalCode();
                if (intervalCode.equals("M")) {
                    addCounter = 1;
                }
                if (intervalCode.equals("Q")) {
                    addCounter = 3;
                }
                /* perform this loop while begin_date is less than or equal to end_date */
                while (!(beginDate.after(endDate))) {
                    beginCalendar.setTime(beginDate);
                    beginCalendar.add(Calendar.MONTH, addCounter);
                    beginDate = KfsDateUtils.convertToSqlDate(beginCalendar.getTime());
                    totalRecurrences++;

                    nextDate = beginDate;
                    nextCalendar.setTime(nextDate);
                    nextCalendar.add(Calendar.MONTH, addCounter);
                    nextDate = KfsDateUtils.convertToSqlDate(nextCalendar.getTime());
                    if (endDate.after(beginDate) && endDate.before(nextDate)) {
                        totalRecurrences++;
                        break;
                    }
                }
                if (totalRecurrences > 0) {
                    this.getCustomerInvoiceRecurrenceDetails().setDocumentTotalRecurrenceNumber(totalRecurrences);
                }
            }

            //  calc end-date if only recurrence-number is specified
            if (ObjectUtils.isNotNull(this.getCustomerInvoiceRecurrenceDetails().getDocumentTotalRecurrenceNumber()) && ObjectUtils.isNull(this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceEndDate())) {

                Calendar beginCalendar = Calendar.getInstance();
                beginCalendar.setTime(new Timestamp(this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceBeginDate().getTime()));
                Calendar endCalendar = Calendar.getInstance();
                endCalendar = beginCalendar;

                int addCounter = 0;
                Integer documentTotalRecurrenceNumber = this.getCustomerInvoiceRecurrenceDetails().getDocumentTotalRecurrenceNumber();
                String intervalCode = this.getCustomerInvoiceRecurrenceDetails().getDocumentRecurrenceIntervalCode();
                if (intervalCode.equals("M")) {
                    addCounter = -1;
                    addCounter += documentTotalRecurrenceNumber * 1;
                }
                if (intervalCode.equals("Q")) {
                    addCounter = -3;
                    addCounter += documentTotalRecurrenceNumber * 3;
                }
                endCalendar.add(Calendar.MONTH, addCounter);
                this.getCustomerInvoiceRecurrenceDetails().setDocumentRecurrenceEndDate(KfsDateUtils.convertToSqlDate(endCalendar.getTime()));
            }
        }

        // Force upper case
        //TODO Force to upper case here since DD forceUpperCase doesn't work. Revert this temp fix after Rice fix.
        setBilledByOrganizationCode(StringUtils.upperCase(billedByOrganizationCode));
        accountsReceivableDocumentHeader.setProcessingOrganizationCode(StringUtils.upperCase(accountsReceivableDocumentHeader.getProcessingOrganizationCode()));
        accountsReceivableDocumentHeader.setCustomerNumber(StringUtils.upperCase(accountsReceivableDocumentHeader.getCustomerNumber()));

        if (ObjectUtils.isNull(getCustomerShipToAddressIdentifier())) {
            setCustomerShipToAddress(null);
            setCustomerShipToAddressOnInvoice(null);
        }

    }

    // returns true only when there is all the required recurrence info
    public boolean getProcessRecurrenceFlag() {
        CustomerInvoiceRecurrenceDetails rec = this.getCustomerInvoiceRecurrenceDetails();

        boolean processRecurrenceFlag = (null != rec.getDocumentRecurrenceIntervalCode());
        processRecurrenceFlag &= (null != rec.getDocumentRecurrenceBeginDate());
        processRecurrenceFlag &= ((null != rec.getDocumentRecurrenceEndDate()) || (null != rec.getDocumentTotalRecurrenceNumber()));
        processRecurrenceFlag &= (rec.isActive());
        processRecurrenceFlag &= (null != rec.getDocumentInitiatorUserIdentifier());

        return processRecurrenceFlag;
    }

    // returns true only if there is no recurrence data at all in recurrence tab
    public boolean getNoRecurrenceDataFlag() {
        CustomerInvoiceRecurrenceDetails rec = this.getCustomerInvoiceRecurrenceDetails();

        boolean noRecurrenceDataFlag = ObjectUtils.isNull(rec.getDocumentRecurrenceIntervalCode());
        noRecurrenceDataFlag &= ObjectUtils.isNull(rec.getDocumentRecurrenceBeginDate());
        noRecurrenceDataFlag &= ObjectUtils.isNull(rec.getDocumentRecurrenceEndDate());
        noRecurrenceDataFlag &= !rec.isActive();
        noRecurrenceDataFlag &= ObjectUtils.isNull(rec.getDocumentTotalRecurrenceNumber());
        noRecurrenceDataFlag &= ObjectUtils.isNull(rec.getDocumentInitiatorUserIdentifier());

        return noRecurrenceDataFlag;
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocumentBase#toCopy()
     */
    @Override
    public void toCopy() throws WorkflowException {
        super.toCopy();
        CustomerInvoiceDocumentService customerInvoiceDocumentService = SpringContext.getBean(CustomerInvoiceDocumentService.class);
        customerInvoiceDocumentService.setupDefaultValuesForCopiedCustomerInvoiceDocument(this);
        this.getFinancialSystemDocumentHeader().setFinancialDocumentTotalAmount(getTotalDollarAmount());
    }

    /**
     * @see org.kuali.kfs.sys.document.GeneralLedgerPostingDocumentBase#toErrorCorrection()
     */
    @Override
    public void toErrorCorrection() throws WorkflowException {
        super.toErrorCorrection();
        negateCustomerInvoiceDetailUnitPrices();
        this.setOpenInvoiceIndicator(false);
        this.getFinancialSystemDocumentHeader().setFinancialDocumentTotalAmount(getTotalDollarAmount());

        //  if we dont force this on the error correction, the recurrence will
        // have the old doc number, and will revert the main doc due to OJB fun,
        // which will cause PK unique index failure.
        if (ObjectUtils.isNotNull(customerInvoiceRecurrenceDetails)) {
            customerInvoiceRecurrenceDetails.setInvoiceNumber(this.documentNumber);
        }
    }


    /**
     * This method...
     */
    @SuppressWarnings("unchecked")
    public void negateCustomerInvoiceDetailUnitPrices() {
        CustomerInvoiceDetail customerInvoiceDetail;
        for (Iterator i = getSourceAccountingLines().iterator(); i.hasNext(); ) {
            customerInvoiceDetail = (CustomerInvoiceDetail) i.next();
            customerInvoiceDetail.setInvoiceItemUnitPrice(customerInvoiceDetail.getInvoiceItemUnitPrice().negate());

            //clear the old CustomerInvoiceDocument
            customerInvoiceDetail.setCustomerInvoiceDocument(null);

            // revert changes for custom invoice error correction
            //SpringContext.getBean(CustomerInvoiceDetailService.class).prepareCustomerInvoiceDetailForErrorCorrection(customerInvoiceDetail, this);
        }
    }

    /**
     * This method returns true if invoice document has at least one discount line
     *
     * @return
     */
    @SuppressWarnings("unchecked")
    public boolean hasAtLeastOneDiscount() {

        CustomerInvoiceDetail customerInvoiceDetail;
        for (Iterator i = getSourceAccountingLines().iterator(); i.hasNext(); ) {
            customerInvoiceDetail = (CustomerInvoiceDetail) i.next();
            if (customerInvoiceDetail.isDiscountLineParent()) {
                return true;
            }
        }
        return false;
    }

    /**
     * This method returns true if line number is discount line number based on sequence number
     *
     * @param sequenceNumber
     * @return
     */
    @SuppressWarnings("unchecked")
    public boolean isDiscountLineBasedOnSequenceNumber(Integer sequenceNumber) {
        if (ObjectUtils.isNull(sequenceNumber)) {
            return false;
        }

        CustomerInvoiceDetail customerInvoiceDetail;
        for (Iterator i = getSourceAccountingLines().iterator(); i.hasNext(); ) {
            customerInvoiceDetail = (CustomerInvoiceDetail) i.next();
            Integer discLineNum = customerInvoiceDetail.getInvoiceItemDiscountLineNumber();

            // check if sequence number is referenced as a discount line for another customer invoice detail (i.e. the parent line)
            if (ObjectUtils.isNotNull(discLineNum) && sequenceNumber.equals(customerInvoiceDetail.getInvoiceItemDiscountLineNumber())) {
                return true;
            }
        }
        return false;
    }

    /**
     * This method returns parent customer invoice detail based on child discount sequence number
     *
     * @param sequenceNumber
     * @return
     */
    @SuppressWarnings("unchecked")
    public CustomerInvoiceDetail getParentLineBasedOnDiscountSequenceNumber(Integer discountSequenceNumber) {

        if (ObjectUtils.isNull(discountSequenceNumber)) {
            return null;
        }

        CustomerInvoiceDetail customerInvoiceDetail;
        for (Iterator i = getSourceAccountingLines().iterator(); i.hasNext(); ) {
            customerInvoiceDetail = (CustomerInvoiceDetail) i.next();
            Integer discLineNum = customerInvoiceDetail.getInvoiceItemDiscountLineNumber();
            if (ObjectUtils.isNotNull(discLineNum) && discountSequenceNumber.equals(customerInvoiceDetail.getInvoiceItemDiscountLineNumber())) {
                return customerInvoiceDetail;
            }
        }
        return null;
    }


    /**
     * This method is called on CustomerInvoiceAction.execute() to set isDiscount to true if it truly is a discount line
     */
    @SuppressWarnings("unchecked")
    public void updateDiscountAndParentLineReferences() {

        CustomerInvoiceDetail discount;
        for (Iterator i = getSourceAccountingLines().iterator(); i.hasNext(); ) {
            discount = (CustomerInvoiceDetail) i.next();

            // get sequence number and check if theres a corresponding parent line for that discount line
            CustomerInvoiceDetail parent = getParentLineBasedOnDiscountSequenceNumber(discount.getSequenceNumber());
            if (ObjectUtils.isNotNull(parent)) {
                discount.setParentDiscountCustomerInvoiceDetail(parent);
                parent.setDiscountCustomerInvoiceDetail(discount);
            } else {
                discount.setParentDiscountCustomerInvoiceDetail(null);
            }
        }
    }

    /**
     * This method removes the corresponding discount line based on the index of the parent line index. This assumes that the
     * discount line is ALWAYS after the index of the parent line.
     *
     * @param deleteIndex
     */
    public void removeDiscountLineBasedOnParentLineIndex(int parentLineIndex) {
        // get parent line line
        CustomerInvoiceDetail parentLine = (CustomerInvoiceDetail) getSourceAccountingLines().get(parentLineIndex);

        // get index for discount line
        int discountLineIndex = -1; // this should ALWAYS get set
        for (int i = 0; i < getSourceAccountingLines().size(); i++) {
            if (parentLine.getInvoiceItemDiscountLineNumber().equals(((CustomerInvoiceDetail) getSourceAccountingLines().get(i)).getSequenceNumber())) {
                discountLineIndex = i;
            }
        }
        // remove discount line
        getSourceAccountingLines().remove(discountLineIndex);
    }

    public CustomerInvoiceRecurrenceDetails getCustomerInvoiceRecurrenceDetails() {
        return customerInvoiceRecurrenceDetails;
    }

    public void setCustomerInvoiceRecurrenceDetails(CustomerInvoiceRecurrenceDetails customerInvoiceRecurrenceDetails) {
        this.customerInvoiceRecurrenceDetails = customerInvoiceRecurrenceDetails;
    }

    public CustomerAddress getCustomerShipToAddress() {
        return customerShipToAddress;
    }

    public void setCustomerShipToAddress(CustomerAddress customerShipToAddress) {
        this.customerShipToAddress = customerShipToAddress;
    }

    public CustomerAddress getCustomerBillToAddress() {
        return customerBillToAddress;
    }

    public void setCustomerBillToAddress(CustomerAddress customerBillToAddress) {
        this.customerBillToAddress = customerBillToAddress;
    }

    public PrintInvoiceOptions getPrintInvoiceOption() {
        if (ObjectUtils.isNull(printInvoiceOption) && StringUtils.isNotEmpty(printInvoiceIndicator)) {
            refreshReferenceObject("printInvoiceOption");
        }
        return printInvoiceOption;
    }

    public void setPrintInvoiceOption(PrintInvoiceOptions printInvoiceOption) {
        this.printInvoiceOption = printInvoiceOption;
    }

    /**
     * This method returns the total of all pre tax amounts for all customer invoice detail lines
     *
     * @return
     */
    public KualiDecimal getInvoiceItemPreTaxAmountTotal() {

        KualiDecimal invoiceItemPreTaxAmountTotal = new KualiDecimal(0);
        for (Iterator i = getSourceAccountingLines().iterator(); i.hasNext(); ) {
            invoiceItemPreTaxAmountTotal = invoiceItemPreTaxAmountTotal.add(((CustomerInvoiceDetail) i.next()).getInvoiceItemPreTaxAmount());
        }
        return invoiceItemPreTaxAmountTotal;
    }

    /**
     * This method returns the total of all tax amounts for all customer invoice detail lines
     *
     * @return
     */
    public KualiDecimal getInvoiceItemTaxAmountTotal() {

        KualiDecimal invoiceItemTaxAmountTotal = new KualiDecimal(0);
        for (Iterator i = getSourceAccountingLines().iterator(); i.hasNext(); ) {
            invoiceItemTaxAmountTotal = invoiceItemTaxAmountTotal.add(((CustomerInvoiceDetail) i.next()).getInvoiceItemTaxAmount());
        }
        return invoiceItemTaxAmountTotal;
    }

    /**
     * This method returns the primary customer address for the customer number provided.
     *
     * @return
     */
    public CustomerAddress getPrimaryAddressForCustomerNumber() {
        if (ObjectUtils.isNotNull(accountsReceivableDocumentHeader) && StringUtils.isNotEmpty(accountsReceivableDocumentHeader.getCustomerNumber())) {
            return SpringContext.getBean(CustomerAddressService.class).getPrimaryAddress(accountsReceivableDocumentHeader.getCustomerNumber());
        }
        return null;
    }

    /**
     * This method returns the customer object for the invoice
     *
     * @return
     */
    public Customer getCustomer() {
        if (ObjectUtils.isNotNull(accountsReceivableDocumentHeader)) {
            return accountsReceivableDocumentHeader.getCustomer();
        }
        return null;
    }

    /**
     * This method will return all the customer invoice details excluding discount invoice detail lines.
     *
     * @return
     */
    public List<CustomerInvoiceDetail> getCustomerInvoiceDetailsWithoutDiscounts() {
        List<CustomerInvoiceDetail> customerInvoiceDetailsWithoutDiscounts = new ArrayList<CustomerInvoiceDetail>();

        updateDiscountAndParentLineReferences();

        List<CustomerInvoiceDetail> customerInvoiceDetailsWithDiscounts = getSourceAccountingLines();
        for (CustomerInvoiceDetail customerInvoiceDetail : customerInvoiceDetailsWithDiscounts) {
            if (!customerInvoiceDetail.isDiscountLine()) {
                customerInvoiceDetail.setDocumentNumber(getDocumentNumber());
                customerInvoiceDetailsWithoutDiscounts.add(customerInvoiceDetail);
            }
        }

        return customerInvoiceDetailsWithoutDiscounts;
    }

    //TODO Andrew
//    /**
//     * This method could be a bit dangerous. It's meant to be used only on the payment application document, where the modified
//     * invoice is never saved.
//     *
//     * @param customerInvoiceDetails
//     */
//    public void setCustomerInvoiceDetailsWithoutDiscounts(List<CustomerInvoiceDetail> customerInvoiceDetails) {
//        List<CustomerInvoiceDetail> customerInvoiceDetailsWithoutDiscounts = getSourceAccountingLines();
//        int sequenceCounter = 0;
//        for (CustomerInvoiceDetail customerInvoiceDetail : customerInvoiceDetailsWithoutDiscounts) {
//            for (CustomerInvoiceDetail revisedCustomerInvoiceDetail : customerInvoiceDetails) {
//                if (!customerInvoiceDetail.isDiscountLine() && customerInvoiceDetail.getSequenceNumber().equals(revisedCustomerInvoiceDetail.getSequenceNumber())) {
//                    customerInvoiceDetailsWithoutDiscounts.remove(sequenceCounter);
//                    customerInvoiceDetailsWithoutDiscounts.add(sequenceCounter, revisedCustomerInvoiceDetail);
//                }
//            }
//            sequenceCounter += 1;
//        }
//        setSourceAccountingLines(customerInvoiceDetailsWithoutDiscounts);
//    }

    /**
     * This method will return all the customer invoice details that are discounts
     *
     * @return
     */
    public List<CustomerInvoiceDetail> getDiscounts() {
        List<CustomerInvoiceDetail> discounts = new ArrayList<CustomerInvoiceDetail>();

        updateDiscountAndParentLineReferences();

        List<CustomerInvoiceDetail> customerInvoiceDetailsWithDiscounts = getSourceAccountingLines();
        for (CustomerInvoiceDetail customerInvoiceDetail : customerInvoiceDetailsWithDiscounts) {
            if (customerInvoiceDetail.isDiscountLine()) {
                customerInvoiceDetail.setDocumentNumber(getDocumentNumber());
                discounts.add(customerInvoiceDetail);
            }
        }

        return discounts;
    }

    @Override
    public int compareTo(CustomerInvoiceDocument customerInvoiceDocument) {
        if (this.getBillByChartOfAccountCode().equals(customerInvoiceDocument.getBillByChartOfAccountCode())) {
            if (this.getBilledByOrganizationCode().equals(customerInvoiceDocument.getBilledByOrganizationCode())) {
                return 0;
            }
        }
        return -1;
    }

    /**
     * Returns whether or not the Invoice would be paid off by applying the additional amount, passed in
     * by the parameter.
     *
     * @param additionalAmountToApply The additional applied amount to test against.
     * @return True if applying the additionalAmountToApply parameter amount would bring the OpenAmount to zero.
     */
    public boolean wouldPayOff(KualiDecimal additionalAmountToApply) {
        KualiDecimal openAmount = getOpenAmount();
        return KualiDecimal.ZERO.isGreaterEqual(openAmount.subtract(additionalAmountToApply));
    }

    @Override
    public KualiDecimal getTotalDollarAmount() {
        return getSourceTotal();
    }

    public String getBillingAddressInternationalProvinceName() {
        return billingAddressInternationalProvinceName;
    }

    @Override
    public void setBillingAddressInternationalProvinceName(String billingAddressInternationalProvinceName) {
        this.billingAddressInternationalProvinceName = billingAddressInternationalProvinceName;
    }

    public String getBillingAddressName() {
        return billingAddressName;
    }

    @Override
    public void setBillingAddressName(String billingAddressName) {
        this.billingAddressName = billingAddressName;
    }

    public String getBillingAddressTypeCode() {
        return billingAddressTypeCode;
    }

    public void setBillingAddressTypeCode(String billingAddressTypeCode) {
        this.billingAddressTypeCode = billingAddressTypeCode;
    }

    public String getBillingCityName() {
        return billingCityName;
    }

    @Override
    public void setBillingCityName(String billingCityName) {
        this.billingCityName = billingCityName;
    }

    public String getBillingCountryCode() {
        return billingCountryCode;
    }

    @Override
    public void setBillingCountryCode(String billingCountryCode) {
        this.billingCountryCode = billingCountryCode;
    }

    public String getBillingEmailAddress() {
        return billingEmailAddress;
    }

    @Override
    public void setBillingEmailAddress(String billingEmailAddress) {
        this.billingEmailAddress = billingEmailAddress;
    }

    public String getBillingInternationalMailCode() {
        return billingInternationalMailCode;
    }

    @Override
    public void setBillingInternationalMailCode(String billingInternationalMailCode) {
        this.billingInternationalMailCode = billingInternationalMailCode;
    }

    public String getBillingStateCode() {
        return billingStateCode;
    }

    @Override
    public void setBillingStateCode(String billingStateCode) {
        this.billingStateCode = billingStateCode;
    }

    public String getBillingZipCode() {
        return billingZipCode;
    }

    @Override
    public void setBillingZipCode(String billingZipCode) {
        this.billingZipCode = billingZipCode;
    }

    @Override
    public String getCustomerName() {
        return customerName;
    }

    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }

    public String getShippingAddressInternationalProvinceName() {
        return shippingAddressInternationalProvinceName;
    }

    public void setShippingAddressInternationalProvinceName(String shippingAddressInternationalProvinceName) {
        this.shippingAddressInternationalProvinceName = shippingAddressInternationalProvinceName;
    }

    public String getShippingAddressName() {
        return shippingAddressName;
    }

    public void setShippingAddressName(String shippingAddressName) {
        this.shippingAddressName = shippingAddressName;
    }

    public String getShippingAddressTypeCode() {
        return shippingAddressTypeCode;
    }

    public void setShippingAddressTypeCode(String shippingAddressTypeCode) {
        this.shippingAddressTypeCode = shippingAddressTypeCode;
    }

    public String getShippingCityName() {
        return shippingCityName;
    }

    public void setShippingCityName(String shippingCityName) {
        this.shippingCityName = shippingCityName;
    }

    public String getShippingCountryCode() {
        return shippingCountryCode;
    }

    public void setShippingCountryCode(String shippingCountryCode) {
        this.shippingCountryCode = shippingCountryCode;
    }

    public String getShippingEmailAddress() {
        return shippingEmailAddress;
    }

    public void setShippingEmailAddress(String shippingEmailAddress) {
        this.shippingEmailAddress = shippingEmailAddress;
    }

    public String getShippingInternationalMailCode() {
        return shippingInternationalMailCode;
    }

    public void setShippingInternationalMailCode(String shippingInternationalMailCode) {
        this.shippingInternationalMailCode = shippingInternationalMailCode;
    }

    public String getShippingStateCode() {
        return shippingStateCode;
    }

    public void setShippingStateCode(String shippingStateCode) {
        this.shippingStateCode = shippingStateCode;
    }

    public String getShippingZipCode() {
        return shippingZipCode;
    }

    public void setShippingZipCode(String shippingZipCode) {
        this.shippingZipCode = shippingZipCode;
    }

    public String getBillingLine1StreetAddress() {
        return billingLine1StreetAddress;
    }

    @Override
    public void setBillingLine1StreetAddress(String billingLine1StreetAddress) {
        this.billingLine1StreetAddress = billingLine1StreetAddress;
    }

    public String getBillingLine2StreetAddress() {
        return billingLine2StreetAddress;
    }

    @Override
    public void setBillingLine2StreetAddress(String billingLine2StreetAddress) {
        this.billingLine2StreetAddress = billingLine2StreetAddress;
    }

    public String getShippingLine1StreetAddress() {
        return shippingLine1StreetAddress;
    }

    public void setShippingLine1StreetAddress(String shippingLine1StreetAddress) {
        this.shippingLine1StreetAddress = shippingLine1StreetAddress;
    }

    public String getShippingLine2StreetAddress() {
        return shippingLine2StreetAddress;
    }

    public void setShippingLine2StreetAddress(String shippingLine2StreetAddress) {
        this.shippingLine2StreetAddress = shippingLine2StreetAddress;
    }

    public boolean getRecurredInvoiceIndicator() {
        return recurredInvoiceIndicator;
    }

    public void setRecurredInvoiceIndicator(boolean recurredInvoiceIndicator) {
        this.recurredInvoiceIndicator = recurredInvoiceIndicator;
    }

    public Date getReportedDate() {
        return reportedDate;
    }

    public void setReportedDate(Date reportedDate) {
        this.reportedDate = reportedDate;
    }

    /**
     * Get a string representation for billing chart/organization
     *
     * @return
     */
    public String getBilledByChartOfAccCodeAndOrgCode() {
        String returnVal = getBillByChartOfAccountCode() + "/" + getBilledByOrganizationCode();

        return returnVal;
    }

    /**
     * Populate Customer Billing Address fields on Customer Invoice.
     *
     * @return
     */
    public void setCustomerBillToAddressOnInvoice(CustomerAddress customerBillToAddress) {
        accountsReceivableDocumentHeader.refreshReferenceObject("customer");
        if (ObjectUtils.isNotNull(accountsReceivableDocumentHeader.getCustomer())) {
            this.setCustomerName(accountsReceivableDocumentHeader.getCustomer().getCustomerName());
        }

        if (ObjectUtils.isNotNull(customerBillToAddress)) {
            this.setBillingAddressTypeCode(customerBillToAddress.getCustomerAddressTypeCode());
            this.setBillingAddressName(customerBillToAddress.getCustomerAddressName());
            this.setBillingLine1StreetAddress(customerBillToAddress.getCustomerLine1StreetAddress());
            this.setBillingLine2StreetAddress(customerBillToAddress.getCustomerLine2StreetAddress());
            this.setBillingCityName(customerBillToAddress.getCustomerCityName());
            this.setBillingStateCode(customerBillToAddress.getCustomerStateCode());
            this.setBillingZipCode(customerBillToAddress.getCustomerZipCode());
            this.setBillingCountryCode(customerBillToAddress.getCustomerCountryCode());
            this.setBillingAddressInternationalProvinceName(customerBillToAddress.getCustomerAddressInternationalProvinceName());
            this.setBillingInternationalMailCode(customerBillToAddress.getCustomerInternationalMailCode());
            this.setBillingEmailAddress(customerBillToAddress.getCustomerEmailAddress());
        }
    }

    /**
     * Populate Customer Shipping Address fields on Customer Invoice.
     *
     * @return
     */
    public void setCustomerShipToAddressOnInvoice(CustomerAddress customerShipToAddress) {

        accountsReceivableDocumentHeader.refreshReferenceObject("customer");
        Customer customer = accountsReceivableDocumentHeader.getCustomer();
        if (ObjectUtils.isNotNull(customer)) {
            this.setCustomerName(customer.getCustomerName());
        }

        if (ObjectUtils.isNotNull(customerShipToAddress)) {
            this.setShippingAddressTypeCode(customerShipToAddress.getCustomerAddressTypeCode());
            this.setShippingAddressName(customerShipToAddress.getCustomerAddressName());
            this.setShippingLine1StreetAddress(customerShipToAddress.getCustomerLine1StreetAddress());
            this.setShippingLine2StreetAddress(customerShipToAddress.getCustomerLine2StreetAddress());
            this.setShippingCityName(customerShipToAddress.getCustomerCityName());
            this.setShippingStateCode(customerShipToAddress.getCustomerStateCode());
            this.setShippingZipCode(customerShipToAddress.getCustomerZipCode());
            this.setShippingCountryCode(customerShipToAddress.getCustomerCountryCode());
            this.setShippingAddressInternationalProvinceName(customerShipToAddress.getCustomerAddressInternationalProvinceName());
            this.setShippingInternationalMailCode(customerShipToAddress.getCustomerInternationalMailCode());
            this.setShippingEmailAddress(customerShipToAddress.getCustomerEmailAddress());
        } else {
            this.setShippingAddressTypeCode(null);
            this.setShippingAddressName(null);
            this.setShippingLine1StreetAddress(null);
            this.setShippingLine2StreetAddress(null);
            this.setShippingCityName(null);
            this.setShippingStateCode(null);
            this.setShippingZipCode(null);
            this.setShippingCountryCode(null);
            this.setShippingAddressInternationalProvinceName(null);
            this.setShippingInternationalMailCode(null);
            this.setShippingEmailAddress(null);
        }
    }

    /**
     * Gets the quickApply attribute.
     *
     * @return Returns the quickApply.
     */
    //TODO Andrew - this is payapp specific stuff and needs to go
//    public boolean isQuickApply() {
//        return quickApply;
//    }
//
//    //TODO Andrew - this is payapp specific stuff and needs to go
//    public boolean getQuickApply() {
//        return isQuickApply();
//    }
//
//    /**
//     * Sets the quickApply attribute value.
//     *
//     * @param quickApply The quickApply to set.
//     */
//    //TODO Andrew - this is payapp specific stuff and needs to go
//    public void setQuickApply(boolean quickApply) {
//        this.quickApply = quickApply;
//    }


    /**
     * Answers true when invoice recurrence details are provided by the user
     *
     * @see org.kuali.kfs.sys.document.FinancialSystemTransactionalDocumentBase#answerSplitNodeQuestion(java.lang.String)
     */
    @Override
    public boolean answerSplitNodeQuestion(String nodeName) throws UnsupportedOperationException {
        if (HAS_RECCURENCE_NODE.equals(nodeName)) {
            return hasRecurrence();
        }
        if (BATCH_GENERATED_NODE.equals(nodeName)) {
            return isBatchGenerated();
        }
        throw new UnsupportedOperationException("answerSplitNode('" + nodeName + "') was called but no handler for nodeName specified.");
    }

    /**
     * Determines whether this document was generated from a recurrence batch.  Returns true if so, false if not.
     *
     * @return
     */
    protected boolean isBatchGenerated() {
        return recurredInvoiceIndicator;
    }

    /**
     * Determines whether this document has a Recurrence filled out enough to create an INVR doc.
     *
     * @return
     */
    protected boolean hasRecurrence() {
        return (ObjectUtils.isNotNull(getCustomerInvoiceRecurrenceDetails()) && getCustomerInvoiceRecurrenceDetails().isActive());
    }

    @Override
    public void setCustomerBillToAddress(AccountsReceivableCustomerAddress customerBillToAddress) {
        this.customerBillToAddress = (CustomerAddress) customerBillToAddress;
    }

    @Override
    public void setBillingAddressTypeCodeAsPrimary() {
        setBillingAddressTypeCode(ArKeyConstants.CustomerConstants.CUSTOMER_ADDRESS_TYPE_CODE_PRIMARY);
    }

    @Override
    public void setCustomerInvoiceRecurrenceDetails(AccountsReceivableCustomerInvoiceRecurrenceDetails customerInvoiceRecurrenceDetails) {
        this.customerInvoiceRecurrenceDetails = (CustomerInvoiceRecurrenceDetails) customerInvoiceRecurrenceDetails;
    }

    @Override
    public void setAccountsReceivableDocumentHeader(org.kuali.kfs.integration.ar.AccountsReceivableDocumentHeader accountsReceivableDocumentHeader) {
        this.accountsReceivableDocumentHeader = (org.kuali.kfs.module.ar.businessobject.AccountsReceivableDocumentHeader) accountsReceivableDocumentHeader;
    }

    private Timestamp agingReportSentTime;

    /**
     * Gets the agingReportSentTime attribute.
     *
     * @return Returns the agingReportSentTime.
     */
    public Timestamp getAgingReportSentTime() {
        return agingReportSentTime;
    }

    /**
     * Sets the agingReportSentTime attribute value.
     *
     * @param agingReportSentTime The agingReportSentTime to set.
     */
    public void setAgingReportSentTime(Timestamp agingReportSentTime) {
        this.agingReportSentTime = agingReportSentTime;
    }
}
