/*
* 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.module.ar.document;
import java.sql.Date;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.Account;
import org.kuali.kfs.coa.businessobject.Chart;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coa.businessobject.ProjectCode;
import org.kuali.kfs.coa.businessobject.SubAccount;
import org.kuali.kfs.coa.businessobject.SubObjectCode;
import org.kuali.kfs.module.ar.ArConstants;
import org.kuali.kfs.module.ar.businessobject.AccountsReceivableDocumentHeader;
import org.kuali.kfs.module.ar.businessobject.CustomerInvoiceDetail;
import org.kuali.kfs.module.ar.businessobject.ReceivableCustomerInvoiceDetail;
import org.kuali.kfs.module.ar.businessobject.SalesTaxCustomerInvoiceDetail;
import org.kuali.kfs.module.ar.businessobject.WriteoffCustomerInvoiceDetail;
import org.kuali.kfs.module.ar.businessobject.WriteoffTaxCustomerInvoiceDetail;
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.CustomerInvoiceWriteoffDocumentService;
import org.kuali.kfs.module.ar.document.service.CustomerService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
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.AmountTotaling;
import org.kuali.kfs.sys.document.GeneralLedgerPendingEntrySource;
import org.kuali.kfs.sys.document.GeneralLedgerPostingDocumentBase;
import org.kuali.kfs.sys.service.GeneralLedgerPendingEntryService;
import org.kuali.kfs.sys.service.TaxService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.coreservice.framework.parameter.ParameterService;
import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
import org.kuali.rice.krad.bo.Note;
import org.kuali.rice.krad.exception.ValidationException;
import org.kuali.rice.krad.rules.rule.event.KualiDocumentEvent;
import org.kuali.rice.krad.util.ObjectUtils;
public class CustomerInvoiceWriteoffDocument extends GeneralLedgerPostingDocumentBase implements GeneralLedgerPendingEntrySource, AmountTotaling {
protected static final String REQUIRES_APPROVAL_NODE = "RequiresApproval";
protected String chartOfAccountsCode;
protected String accountNumber;
protected String subAccountNumber;
protected String financialObjectCode;
protected String financialSubObjectCode;
protected String projectCode;
protected String organizationReferenceIdentifier;
protected String financialDocumentReferenceInvoiceNumber;
protected String statusCode;
protected String customerNote;
protected Account account;
protected Chart chartOfAccounts;
protected SubAccount subAccount;
protected ObjectCode financialObject;
protected SubObjectCode financialSubObject;
protected ProjectCode project;
protected CustomerInvoiceDocument customerInvoiceDocument;
protected AccountsReceivableDocumentHeader accountsReceivableDocumentHeader;
protected KualiDecimal invoiceWriteoffAmount;
// GLPEs from invoice to be written off - used for collecting tax amounts that should be written off
protected class WriteOffGlpes {
GeneralLedgerPendingEntry glpe;
boolean isWriteOffDetail;
public WriteOffGlpes(GeneralLedgerPendingEntry pe, boolean writeOff) {
this.glpe = pe;
this.isWriteOffDetail = writeOff;
}
}
public AccountsReceivableDocumentHeader getAccountsReceivableDocumentHeader() {
return accountsReceivableDocumentHeader;
}
public void setAccountsReceivableDocumentHeader(AccountsReceivableDocumentHeader accountsReceivableDocumentHeader) {
this.accountsReceivableDocumentHeader = accountsReceivableDocumentHeader;
}
public String getChartOfAccountsCode() {
return chartOfAccountsCode;
}
public void setChartOfAccountsCode(String chartOfAccountsCode) {
this.chartOfAccountsCode = chartOfAccountsCode;
}
public String getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(String accountNumber) {
this.accountNumber = accountNumber;
}
public String getSubAccountNumber() {
return subAccountNumber;
}
public void setSubAccountNumber(String subAccountNumber) {
this.subAccountNumber = subAccountNumber;
}
public String getFinancialObjectCode() {
return financialObjectCode;
}
public void setFinancialObjectCode(String financialObjectCode) {
this.financialObjectCode = financialObjectCode;
}
public String getFinancialSubObjectCode() {
return financialSubObjectCode;
}
public void setFinancialSubObjectCode(String financialSubObjectCode) {
this.financialSubObjectCode = financialSubObjectCode;
}
public String getProjectCode() {
return projectCode;
}
public void setProjectCode(String projectCode) {
this.projectCode = projectCode;
}
public String getOrganizationReferenceIdentifier() {
return organizationReferenceIdentifier;
}
public void setOrganizationReferenceIdentifier(String organizationReferenceIdentifier) {
this.organizationReferenceIdentifier = organizationReferenceIdentifier;
}
public String getFinancialDocumentReferenceInvoiceNumber() {
return financialDocumentReferenceInvoiceNumber;
}
public void setFinancialDocumentReferenceInvoiceNumber(String financialDocumentReferenceInvoiceNumber) {
this.financialDocumentReferenceInvoiceNumber = financialDocumentReferenceInvoiceNumber;
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public Chart getChartOfAccounts() {
return chartOfAccounts;
}
public void setChartOfAccounts(Chart chartOfAccounts) {
this.chartOfAccounts = chartOfAccounts;
}
public SubAccount getSubAccount() {
return subAccount;
}
public void setSubAccount(SubAccount subAccount) {
this.subAccount = subAccount;
}
public ObjectCode getFinancialObject() {
return financialObject;
}
public void setFinancialObject(ObjectCode financialObject) {
this.financialObject = financialObject;
}
public SubObjectCode getFinancialSubObject() {
return financialSubObject;
}
public void setFinancialSubObject(SubObjectCode financialSubObject) {
this.financialSubObject = financialSubObject;
}
public ProjectCode getProject() {
return project;
}
public void setProject(ProjectCode project) {
this.project = project;
}
public CustomerInvoiceDocument getCustomerInvoiceDocument() {
if (ObjectUtils.isNull(customerInvoiceDocument) && StringUtils.isNotEmpty(financialDocumentReferenceInvoiceNumber)) {
refreshReferenceObject("customerInvoiceDocument");
}
return customerInvoiceDocument;
}
public void setCustomerInvoiceDocument(CustomerInvoiceDocument customerInvoiceDocument) {
this.customerInvoiceDocument = customerInvoiceDocument;
}
/**
* This method returns all the applicable invoice details for writeoff. This method also sets the writeoff document number on
* each of the invoice details for making retrieval of the actual writeoff amount easier.
*
* @return
*/
public List<CustomerInvoiceDetail> getCustomerInvoiceDetailsForWriteoff() {
List<CustomerInvoiceDetail> customerInvoiceDetailsForWriteoff = new ArrayList<CustomerInvoiceDetail>();
for (CustomerInvoiceDetail customerInvoiceDetail : getCustomerInvoiceDocument().getCustomerInvoiceDetailsWithoutDiscounts()) {
customerInvoiceDetail.setCustomerInvoiceWriteoffDocumentNumber(this.documentNumber);
customerInvoiceDetailsForWriteoff.add(customerInvoiceDetail);
}
return customerInvoiceDetailsForWriteoff;
}
/**
* This method returns the total amount to be written off
*
* @return
*/
public KualiDecimal getInvoiceWriteoffAmount() {
// only pull the invoice open amount as the invoice writeoff amount while the doc
// is in play. once its been approved, rely on the amount stored in the db.
if (!KFSConstants.DocumentStatusCodes.APPROVED.equals(getFinancialSystemDocumentHeader().getFinancialDocumentStatusCode())) {
invoiceWriteoffAmount = customerInvoiceDocument.getOpenAmount();
}
return invoiceWriteoffAmount;
}
public void setInvoiceWriteoffAmount(KualiDecimal invoiceWriteoffAmount) {
this.invoiceWriteoffAmount = invoiceWriteoffAmount;
}
public String getStatusCode() {
return statusCode;
}
public void setStatusCode(String statusCode) {
this.statusCode = statusCode;
}
/**
* Initializes the values for a new document.
*/
public void initiateDocument() {
setStatusCode(ArConstants.CustomerInvoiceWriteoffStatuses.INITIATE);
}
/**
* Clear out the initially populated fields.
*/
public void clearInitFields() {
setFinancialDocumentReferenceInvoiceNumber(null);
}
@Override
public List<String> getWorkflowEngineDocumentIdsToLock() {
if (StringUtils.isNotBlank(getFinancialDocumentReferenceInvoiceNumber())) {
List<String> documentIds = new ArrayList<String>();
documentIds.add(getFinancialDocumentReferenceInvoiceNumber());
return documentIds;
}
return null;
}
/**
* When document is processed do the following: 1) Apply amounts to writeoff invoice 2) Mark off invoice indiciator
*
* @see org.kuali.kfs.sys.document.GeneralLedgerPostingDocumentBase#doRouteStatusChange()
*/
@Override
public void doRouteStatusChange(DocumentRouteStatusChange statusChangeEvent) {
super.doRouteStatusChange(statusChangeEvent);
if (getDocumentHeader().getWorkflowDocument().isProcessed()) {
CustomerInvoiceWriteoffDocumentService writeoffService = SpringContext.getBean(CustomerInvoiceWriteoffDocumentService.class);
writeoffService.completeWriteoffProcess(this);
}
}
/**
* do all the calculations before the document gets saved gets called for 'Submit', 'Save', and 'Blanket Approved'
*
* @see org.kuali.rice.krad.document.Document#prepareForSave(org.kuali.rice.krad.rule.event.KualiDocumentEvent)
*/
@Override
public void prepareForSave(KualiDocumentEvent event) {
// generate GLPEs
if (!SpringContext.getBean(GeneralLedgerPendingEntryService.class).generateGeneralLedgerPendingEntries(this)) {
logErrors();
throw new ValidationException("general ledger GLPE generation failed");
}
super.prepareForSave(event);
}
@Override
public boolean generateDocumentGeneralLedgerPendingEntries(GeneralLedgerPendingEntrySequenceHelper sequenceHelper) {
return true;
}
/**
* This method creates the following GLPE's for the customer invoice writeoff 1. C Receivable object code with remaining amount
* 2. D Writeoff object code (or Writeoff FAU) with remaining amount 3. C Receivable object code for tax on state tax account 4.
* D Sales Tax object code for tax on the state tax account 5. C Receivable object code for tax on district tax account 6. D
* District tax object code for $1.00 on the district tax account
*
* @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) {
CustomerInvoiceDetail customerInvoiceDetail = (CustomerInvoiceDetail) glpeSourceDetail;
// if invoice item open amount <= 0 -> do not generate GLPEs for this glpeSourceDetail
if (!customerInvoiceDetail.getAmountOpen().isPositive()) {
return true;
}
KualiDecimal amount;
String writeoffTaxGenerationOption = SpringContext.getBean(ParameterService.class).getParameterValueAsString(CustomerInvoiceWriteoffDocument.class, ArConstants.ALLOW_SALES_TAX_LIABILITY_ADJUSTMENT_IND);
boolean hasWriteoffTaxClaimOnCashOffset = ArConstants.ALLOW_SALES_TAX_LIABILITY_ADJUSTMENT_IND_NO.equals(writeoffTaxGenerationOption);
// if sales tax is enabled generate tax GLPEs
if (SpringContext.getBean(AccountsReceivableTaxService.class).isCustomerInvoiceDetailTaxable(getCustomerInvoiceDocument(), customerInvoiceDetail)) {
amount = customerInvoiceDetail.getInvoiceItemPreTaxAmount();
addReceivableGLPEs(sequenceHelper, glpeSourceDetail, true, amount);
sequenceHelper.increment();
addWriteoffGLPEs(sequenceHelper, glpeSourceDetail, true, amount);
addSalesTaxGLPEs(sequenceHelper, glpeSourceDetail, true, amount);
}
else {
amount = customerInvoiceDetail.getAmountOpen();
addReceivableGLPEs(sequenceHelper, glpeSourceDetail, true, amount);
sequenceHelper.increment();
addWriteoffGLPEs(sequenceHelper, glpeSourceDetail, true, amount);
}
return true;
}
/**
* This method creates the receivable GLPEs for customer invoice details using the remaining amount
*
* @param poster
* @param sequenceHelper
* @param postable
* @param explicitEntry
*/
protected void addReceivableGLPEs(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, boolean hasClaimOnCashOffset, KualiDecimal amount) {
CustomerInvoiceDetail customerInvoiceDetail = (CustomerInvoiceDetail) glpeSourceDetail;
ReceivableCustomerInvoiceDetail receivableCustomerInvoiceDetail = new ReceivableCustomerInvoiceDetail(customerInvoiceDetail, getCustomerInvoiceDocument());
boolean isDebit = false;
AccountsReceivablePendingEntryService service = SpringContext.getBean(AccountsReceivablePendingEntryService.class);
service.createAndAddGenericInvoiceRelatedGLPEs(this, receivableCustomerInvoiceDetail, sequenceHelper, isDebit, hasClaimOnCashOffset, amount);
}
/**
* This method adds writeoff GLPE's for the customer invoice details using the remaining amount.
*
* @param poster
* @param sequenceHelper
* @param postable
* @param explicitEntry
*/
protected void addWriteoffGLPEs(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, boolean hasClaimOnCashOffset, KualiDecimal amount) {
CustomerInvoiceDetail customerInvoiceDetail = (CustomerInvoiceDetail) glpeSourceDetail;
WriteoffCustomerInvoiceDetail writeoffCustomerInvoiceDetail = new WriteoffCustomerInvoiceDetail(customerInvoiceDetail, this);
boolean isDebit = true;
AccountsReceivablePendingEntryService service = SpringContext.getBean(AccountsReceivablePendingEntryService.class);
service.createAndAddGenericInvoiceRelatedGLPEs(this, writeoffCustomerInvoiceDetail, sequenceHelper, isDebit, hasClaimOnCashOffset, amount);
}
protected void addSalesTaxGLPEs(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, boolean hasWriteoffTaxClaimOnCashOffset, KualiDecimal amount) {
List<GeneralLedgerPendingEntry> invGlpes = getCustomerInvoiceDocument().getGeneralLedgerPendingEntries();
CustomerInvoiceDetail customerInvoiceDetail = (CustomerInvoiceDetail) glpeSourceDetail;
WriteoffCustomerInvoiceDetail writeoffCustomerInvoiceDetail = new WriteoffCustomerInvoiceDetail(customerInvoiceDetail, this);
boolean isDebit = true;
String postalCode = SpringContext.getBean(AccountsReceivableTaxService.class).getPostalCodeForTaxation(getCustomerInvoiceDocument());
Date dateOfTransaction = getCustomerInvoiceDocument().getBillingDate();
List<TaxDetail> salesTaxDetails = SpringContext.getBean(TaxService.class).getSalesTaxDetails(dateOfTransaction, postalCode, amount);
AccountsReceivablePendingEntryService service = SpringContext.getBean(AccountsReceivablePendingEntryService.class);
SalesTaxCustomerInvoiceDetail salesTaxCustomerInvoiceDetail;
ReceivableCustomerInvoiceDetail receivableCustomerInvoiceDetail;
WriteoffTaxCustomerInvoiceDetail writeoffTaxCustomerInvoiceDetail;
for (TaxDetail salesTaxDetail : salesTaxDetails) {
salesTaxCustomerInvoiceDetail = new SalesTaxCustomerInvoiceDetail(salesTaxDetail, customerInvoiceDetail);
receivableCustomerInvoiceDetail = new ReceivableCustomerInvoiceDetail(salesTaxCustomerInvoiceDetail, getCustomerInvoiceDocument());
writeoffTaxCustomerInvoiceDetail = new WriteoffTaxCustomerInvoiceDetail(salesTaxCustomerInvoiceDetail, this);
CustomerInvoiceDetail customerInvDetail = (hasWriteoffTaxClaimOnCashOffset) ? writeoffTaxCustomerInvoiceDetail : salesTaxCustomerInvoiceDetail;
List<WriteOffGlpes> newGlpes = findGeneralLedgerPendingEntryForDetail(invGlpes, receivableCustomerInvoiceDetail, salesTaxCustomerInvoiceDetail);
for (WriteOffGlpes writeOffGlpe : newGlpes) {
sequenceHelper.increment();
if (writeOffGlpe.isWriteOffDetail) {
service.createAndAddGenericInvoiceRelatedGLPEs(this, customerInvDetail, sequenceHelper, isDebit, hasWriteoffTaxClaimOnCashOffset, writeOffGlpe.glpe.getTransactionLedgerEntryAmount());
}
else {
service.createAndAddGenericInvoiceRelatedGLPEs(this, receivableCustomerInvoiceDetail, sequenceHelper, !isDebit, hasWriteoffTaxClaimOnCashOffset, writeOffGlpe.glpe.getTransactionLedgerEntryAmount());
}
invGlpes.remove(writeOffGlpe.glpe);
}
}
}
/** find all glpes that match this sales tax entry
*
*
* @param glpes
* @param glpeSourceDetail
* @param glpeWriteOffDetail
* @return array returned of all matches glpes
*/
protected List<WriteOffGlpes> findGeneralLedgerPendingEntryForDetail(List<GeneralLedgerPendingEntry> glpes, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, GeneralLedgerPendingEntrySourceDetail glpeWriteOffDetail) {
int counter = 0;
ArrayList<WriteOffGlpes> arrGlpeMatches = new ArrayList<WriteOffGlpes>();
for (GeneralLedgerPendingEntry glpe : glpes) {
if ((glpeSourceDetail.getAccountNumber().matches(glpe.getAccountNumber())) && (glpeSourceDetail.getChartOfAccountsCode().matches(glpe.getChartOfAccountsCode())) && (glpeSourceDetail.getObjectCode().getFinancialObjectCode().matches(glpe.getFinancialObjectCode()))) {
arrGlpeMatches.add(new WriteOffGlpes(glpe, false));
}
else if ((glpeWriteOffDetail.getAccountNumber().matches(glpe.getAccountNumber())) && (glpeWriteOffDetail.getChartOfAccountsCode().matches(glpe.getChartOfAccountsCode())) && (glpeWriteOffDetail.getObjectCode().getFinancialObjectCode().matches(glpe.getFinancialObjectCode()))) {
arrGlpeMatches.add(new WriteOffGlpes(glpe, true));
}
}
return arrGlpeMatches;
}
@Override
public KualiDecimal getGeneralLedgerPendingEntryAmountForDetail(GeneralLedgerPendingEntrySourceDetail glpeSourceDetail) {
return null;
}
@Override
public List<GeneralLedgerPendingEntrySourceDetail> getGeneralLedgerPendingEntrySourceDetails() {
List<GeneralLedgerPendingEntrySourceDetail> generalLedgerPendingEntrySourceDetails = new ArrayList<GeneralLedgerPendingEntrySourceDetail>();
generalLedgerPendingEntrySourceDetails.addAll(getCustomerInvoiceDocument().getCustomerInvoiceDetailsWithoutDiscounts());
return generalLedgerPendingEntrySourceDetails;
}
@Override
public boolean isDebit(GeneralLedgerPendingEntrySourceDetail postable) {
// TODO Auto-generated method stub
return false;
}
@Override
public KualiDecimal getTotalDollarAmount() {
return getInvoiceWriteoffAmount();
}
/**
* Gets the customerNote attribute.
*
* @return Returns the customerNote.
*/
public String getCustomerNote() {
return customerNote;
}
/**
* Sets the customerNote attribute value.
*
* @param customerNote The customerNote to set.
*/
public void setCustomerNote(String customerNote) {
this.customerNote = customerNote;
}
public void populateCustomerNote() {
customerNote = "";
CustomerService customerService = SpringContext.getBean(CustomerService.class);
List<Note> boNotes = customerService.getCustomerNotes(this.getCustomerInvoiceDocument().getCustomer().getCustomerNumber());
StringBuffer customerNotes = new StringBuffer();
if (boNotes.size() > 0) {
for (Note note : boNotes) {
customerNotes.append(note.getNoteText().trim() + " ");
}
this.customerNote = customerNotes.toString().trim();
}
}
/**
* Answers true when invoice write off amount is greater than default approved amount ($50???)
*
* @see org.kuali.kfs.sys.document.FinancialSystemTransactionalDocumentBase#answerSplitNodeQuestion(java.lang.String)
*/
@Override
public boolean answerSplitNodeQuestion(String nodeName) throws UnsupportedOperationException {
if (REQUIRES_APPROVAL_NODE.equals(nodeName)) {
// grab the approval threshold from the param service
ParameterService paramService = SpringContext.getBean(ParameterService.class);
KualiDecimal approvalThreshold = new KualiDecimal(paramService.getParameterValueAsString(CustomerInvoiceWriteoffDocument.class, ArConstants.WRITEOFF_APPROVAL_THRESHOLD));
return (approvalThreshold.isLessThan(getInvoiceWriteoffAmount()));
}
throw new UnsupportedOperationException("answerSplitNode('" + nodeName + "') called but no handler for this nodeName present.");
}
}