/* * 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.LinkedHashMap; import java.util.List; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.module.ar.ArConstants; import org.kuali.kfs.module.ar.ArKeyConstants; import org.kuali.kfs.module.ar.businessobject.CollectionEvent; import org.kuali.kfs.module.ar.businessobject.ContractsGrantsInvoiceDetail; import org.kuali.kfs.module.ar.businessobject.CustomerInvoiceDetail; import org.kuali.kfs.module.ar.businessobject.InvoiceAccountDetail; import org.kuali.kfs.module.ar.businessobject.InvoiceAddressDetail; import org.kuali.kfs.module.ar.businessobject.InvoiceBill; import org.kuali.kfs.module.ar.businessobject.InvoiceDetailAccountObjectCode; import org.kuali.kfs.module.ar.businessobject.InvoiceGeneralDetail; import org.kuali.kfs.module.ar.businessobject.InvoiceMilestone; import org.kuali.kfs.module.ar.businessobject.InvoiceSuspensionCategory; import org.kuali.kfs.module.ar.document.service.ContractsGrantsInvoiceDocumentService; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.KFSKeyConstants; import org.kuali.kfs.sys.KFSPropertyConstants; import org.kuali.kfs.sys.context.SpringContext; 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 org.kuali.rice.krad.service.DocumentService; import org.kuali.rice.krad.util.GlobalVariables; import org.kuali.rice.krad.util.ObjectUtils; /** * Contracts & Grants Invoice document extending Customer Invoice document. */ public class ContractsGrantsInvoiceDocument extends CustomerInvoiceDocument { protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ContractsGrantsInvoiceDocument.class); private KualiDecimal paymentAmount = KualiDecimal.ZERO; private KualiDecimal balanceDue = KualiDecimal.ZERO; private List<ContractsGrantsInvoiceDetail> invoiceDetails; private List<CollectionEvent> collectionEvents; private List<InvoiceDetailAccountObjectCode> invoiceDetailAccountObjectCodes; private List<InvoiceAddressDetail> invoiceAddressDetails; private List<InvoiceAccountDetail> accountDetails; private InvoiceGeneralDetail invoiceGeneralDetail; private List<InvoiceMilestone> invoiceMilestones; private List<InvoiceBill> invoiceBills; private List<InvoiceSuspensionCategory> invoiceSuspensionCategories; private final String REQUIRES_APPROVAL_SPLIT = "RequiresApprovalSplit"; /** * Default constructor. */ public ContractsGrantsInvoiceDocument() { invoiceAddressDetails = new ArrayList<InvoiceAddressDetail>(); invoiceDetails = new ArrayList<ContractsGrantsInvoiceDetail>(); collectionEvents = new ArrayList<CollectionEvent>(); accountDetails = new ArrayList<InvoiceAccountDetail>(); invoiceMilestones = new ArrayList<InvoiceMilestone>(); invoiceBills = new ArrayList<InvoiceBill>(); invoiceDetailAccountObjectCodes = new ArrayList<InvoiceDetailAccountObjectCode>(); invoiceSuspensionCategories = new ArrayList<InvoiceSuspensionCategory>(); } /** * Gets the finalizable attribute. * * @return Returns the finalizable. */ public boolean isFinalizable() { if (this == null) { return false; } return this.getDocumentHeader().getWorkflowDocument().getDateCreated().isAfter(getInvoiceGeneralDetail().getAward().getAwardEndingDate().getTime()); } /** * This method returns true if this is a correction document * * @return is Error Correction Document */ public boolean isCorrectionDocument() { return !StringUtils.isEmpty(this.getFinancialSystemDocumentHeader().getFinancialDocumentInErrorNumber()); } /** * @see org.kuali.rice.krad.bo.PersistableBusinessObjectBase#buildListOfDeletionAwareLists() */ @Override public List buildListOfDeletionAwareLists() { List deletionAwareLists = super.buildListOfDeletionAwareLists(); if (invoiceSuspensionCategories != null) { deletionAwareLists.add(invoiceSuspensionCategories); } return deletionAwareLists; } /** * @see org.kuali.kfs.module.ar.document.CustomerInvoiceDocument#prepareForSave() */ @Override public void prepareForSave() { super.prepareForSave(); // To do a recalculate of current expenditures in invoice details section so that the totals get affected properly. // To be performed whenever the document is saved only for awards without Milestones or Bills if (!StringUtils.equalsIgnoreCase(getInvoiceGeneralDetail().getBillingFrequencyCode(), ArConstants.MILESTONE_BILLING_SCHEDULE_CODE) && !StringUtils.equalsIgnoreCase(getInvoiceGeneralDetail().getBillingFrequencyCode(), ArConstants.PREDETERMINED_BILLING_SCHEDULE_CODE)) { ContractsGrantsInvoiceDocumentService contractsGrantsInvoiceDocumentService = SpringContext.getBean(ContractsGrantsInvoiceDocumentService.class); contractsGrantsInvoiceDocumentService.recalculateTotalAmountBilledToDate(this); } } /** * @see org.kuali.kfs.module.ar.document.CustomerInvoiceDocument#doRouteStatusChange(org.kuali.rice.kew.dto.DocumentRouteStatusChangeDTO) */ @Override public void doRouteStatusChange(DocumentRouteStatusChange statusChangeEvent) { super.doRouteStatusChange(statusChangeEvent); ContractsGrantsInvoiceDocumentService contractsGrantsInvoiceDocumentService = SpringContext.getBean(ContractsGrantsInvoiceDocumentService.class); if ( getDocumentHeader().getWorkflowDocument().isProcessed() ) { // update award accounts to final billed contractsGrantsInvoiceDocumentService.updateLastBilledDate(this); if (isInvoiceReversal()) { // Invoice correction process when corrected invoice goes to FINAL try { getInvoiceGeneralDetail().setFinalBillIndicator(false); ContractsGrantsInvoiceDocument invoice = (ContractsGrantsInvoiceDocument) SpringContext.getBean(DocumentService.class).getByDocumentHeaderId(this.getFinancialSystemDocumentHeader().getFinancialDocumentInErrorNumber()); if (ObjectUtils.isNotNull(invoice)) { invoice.setInvoiceDueDate(new Date(new java.util.Date().getTime())); invoice.getInvoiceGeneralDetail().setFinalBillIndicator(false); SpringContext.getBean(DocumentService.class).updateDocument(invoice); // update correction to the AwardAccount Objects since the Invoice was unmarked as Final contractsGrantsInvoiceDocumentService.updateUnfinalizationToAwardAccount(invoice.getAccountDetails(),invoice.getInvoiceGeneralDetail().getProposalNumber()); getInvoiceGeneralDetail().setLastBilledDate(null);// Set invoice last billed date to null. if (invoice.getInvoiceGeneralDetail().getBillingFrequencyCode().equals(ArConstants.MILESTONE_BILLING_SCHEDULE_CODE)) { contractsGrantsInvoiceDocumentService.updateMilestonesBilledIndicator(false,invoice.getInvoiceMilestones()); } else if (invoice.getInvoiceGeneralDetail().getBillingFrequencyCode().equals(ArConstants.PREDETERMINED_BILLING_SCHEDULE_CODE)) { contractsGrantsInvoiceDocumentService.updateBillsBilledIndicator(false,invoice.getInvoiceBills()); } } else { GlobalVariables.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.ERROR_CORRECTED_INVOICE_NOT_FOUND_ERROR, ArKeyConstants.CORRECTED_INVOICE_NOT_FOUND_ERROR); } } catch (WorkflowException ex) { LOG.error("problem during ContractsGrantsInvoiceDocument.doRouteStatusChange()", ex); throw new RuntimeException("WorkflowException during ContractsGrantsInvoiceDocument.doRouteStatusChange()", ex); // if KEW is down, how are we even here? bad data that should no longer exist or something? } } else { // update Milestones and Bills when invoice goes to final state contractsGrantsInvoiceDocumentService.updateBillsAndMilestones(true, invoiceMilestones, invoiceBills); // generate the invoices from templates contractsGrantsInvoiceDocumentService.generateInvoicesForInvoiceAddresses(this); } contractsGrantsInvoiceDocumentService.addToAccountObjectCodeBilledTotal(invoiceDetailAccountObjectCodes); } } /** * Make changes here to implement what needs to be done when correction button is clicked * * @see org.kuali.kfs.module.ar.document.CustomerInvoiceDocument#toErrorCorrection() */ @Override public void toErrorCorrection() throws WorkflowException { super.toErrorCorrection(); invoiceSuspensionCategories.clear(); SpringContext.getBean(ContractsGrantsInvoiceDocumentService.class).correctContractsGrantsInvoiceDocument(this); } /** * Gets the list of invoice Details without the Total fields or any indirect cost categories * * @return Returns the invoiceDetails. */ public List<ContractsGrantsInvoiceDetail> getDirectCostInvoiceDetails() { List<ContractsGrantsInvoiceDetail> invDetails = new ArrayList<ContractsGrantsInvoiceDetail>(); for (ContractsGrantsInvoiceDetail invD : invoiceDetails) { if (!invD.isIndirectCostIndicator()) { invDetails.add(invD); } } return invDetails; } /** * This method returns a list of invoice details which are indirect costs only. * These invoice details are not shown on the document and is different from the * other method getInDirectCostInvoiceDetails() because that method returns the total. */ public List<ContractsGrantsInvoiceDetail> getIndirectCostInvoiceDetails(){ List<ContractsGrantsInvoiceDetail> invDetails = new ArrayList<ContractsGrantsInvoiceDetail>(); for (ContractsGrantsInvoiceDetail invD : invoiceDetails) { if (invD.isIndirectCostIndicator()) { invDetails.add(invD); } } return invDetails; } /** * Gets the invoiceDetails attribute value. * * @param invoiceDetails The invoiceDetails to set. */ public List<ContractsGrantsInvoiceDetail> getInvoiceDetails() { return invoiceDetails; } /** * Sets the invoiceDetails attribute value. * * @param invoiceDetails The invoiceDetails to set. */ public void setInvoiceDetails(List<ContractsGrantsInvoiceDetail> invoiceDetails) { this.invoiceDetails = invoiceDetails; } /** * @return */ public List<InvoiceDetailAccountObjectCode> getInvoiceDetailAccountObjectCodes() { return invoiceDetailAccountObjectCodes; } /** * @param invoiceDetailAccountObjectCodes */ public void setInvoiceDetailAccountObjectCodes(List<InvoiceDetailAccountObjectCode> invoiceDetailAccountObjectCodes) { this.invoiceDetailAccountObjectCodes = invoiceDetailAccountObjectCodes; } /** * Gets the invoiceAddressDetails attribute. * * @return Returns the invoiceAddressDetails. */ public List<InvoiceAddressDetail> getInvoiceAddressDetails() { return invoiceAddressDetails; } /** * Sets the invoiceAddressDetails attribute value. * * @param invoiceAddressDetails The invoiceAddressDetails to set. */ public void setInvoiceAddressDetails(List<InvoiceAddressDetail> invoiceAddressDetails) { this.invoiceAddressDetails = invoiceAddressDetails; } /** * Gets the accountDetails attribute. * * @return Returns the accountDetails. */ public List<InvoiceAccountDetail> getAccountDetails() { return accountDetails; } /** * Sets the accountDetails attribute value. * * @param accountDetails The accountDetails to set. */ public void setAccountDetails(List<InvoiceAccountDetail> accountDetails) { this.accountDetails = accountDetails; } /** * 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; } /** * @see org.kuali.rice.krad.bo.BusinessObjectBase#toStringMapper() */ @Override @SuppressWarnings("unchecked") protected LinkedHashMap toStringMapper_RICE20_REFACTORME() { LinkedHashMap m = new LinkedHashMap(); m.put(KFSPropertyConstants.DOCUMENT_NUMBER, this.documentNumber); m.put("invoiceGeneralDetail", invoiceGeneralDetail); return m; } /** * Gets the invoiceGeneralDetail attribute. * * @return Returns the invoiceGeneralDetail. */ public InvoiceGeneralDetail getInvoiceGeneralDetail() { return invoiceGeneralDetail; } /** * Sets the invoiceGeneralDetail attribute value. * * @param invoiceGeneralDetail The invoiceGeneralDetail to set. */ public void setInvoiceGeneralDetail(InvoiceGeneralDetail invoiceGeneralDetail) { this.invoiceGeneralDetail = invoiceGeneralDetail; } /** * Gets the invoiceMilestones attribute. * * @return Returns the invoiceMilestones. */ public List<InvoiceMilestone> getInvoiceMilestones() { return invoiceMilestones; } /** * Sets the invoiceMilestones attribute value. * * @param invoiceMilestones The invoiceMilestones to set. */ public void setInvoiceMilestones(List<InvoiceMilestone> invoiceMilestones) { this.invoiceMilestones = invoiceMilestones; } /** * Gets the invoiceBills attribute. * * @return Returns the invoiceBills. */ public List<InvoiceBill> getInvoiceBills() { return invoiceBills; } /** * Sets the invoiceBills attribute value. * * @param invoiceBills The invoiceBills to set. */ public void setInvoiceBills(List<InvoiceBill> invoiceBills) { this.invoiceBills = invoiceBills; } /** * Gets the Total Direct Cost InvoiceDetail. * * @return Returns the total direct Cost InvoiceDetails. */ public ContractsGrantsInvoiceDetail getTotalDirectCostInvoiceDetail() { ContractsGrantsInvoiceDetail totalDirectCostInvoiceDetail = new ContractsGrantsInvoiceDetail(); for (ContractsGrantsInvoiceDetail currentInvoiceDetail : getDirectCostInvoiceDetails()) { totalDirectCostInvoiceDetail.sumInvoiceDetail(currentInvoiceDetail); } return totalDirectCostInvoiceDetail; } /** * Gets the list of Collection Events. * * @return Returns the collectionEvents. */ public List<CollectionEvent> getCollectionEvents() { return collectionEvents; } /** * Sets the list of Collection Events. * * @param events The collectionEvents to set. */ public void setCollectionEvents(List<CollectionEvent> collectionEvents) { this.collectionEvents = collectionEvents; } /** * Generate the next Collection Event Code by concatenating a the count of current events +1 formatted, to the * document number for this invoice. * * @return next collection event code */ public String getNextCollectionEventCode() { String nextCollectionEventCode = documentNumber + "-" + String.format("%03d", collectionEvents.size() + 1); return nextCollectionEventCode; } /** * Gets the total indirect cost InvoiceDetail * * @return Returns the total indirect cost InvoiceDetail. */ public ContractsGrantsInvoiceDetail getTotalIndirectCostInvoiceDetail() { ContractsGrantsInvoiceDetail totalInDirectCostInvoiceDetail = new ContractsGrantsInvoiceDetail(); for (ContractsGrantsInvoiceDetail currentInvoiceDetail : getIndirectCostInvoiceDetails()) { totalInDirectCostInvoiceDetail.sumInvoiceDetail(currentInvoiceDetail); } return totalInDirectCostInvoiceDetail; } /** * Gets the total cost InvoiceDetail. * * @return Returns the total cost InvoiceDetail. */ public ContractsGrantsInvoiceDetail getTotalCostInvoiceDetail() { ContractsGrantsInvoiceDetail totalCostInvoiceDetail = new ContractsGrantsInvoiceDetail(); totalCostInvoiceDetail.sumInvoiceDetail(getTotalDirectCostInvoiceDetail()); totalCostInvoiceDetail.sumInvoiceDetail(getTotalIndirectCostInvoiceDetail()); return totalCostInvoiceDetail; } /** * @return */ public List<InvoiceSuspensionCategory> getInvoiceSuspensionCategories() { return invoiceSuspensionCategories; } /** * @param invoiceSuspensionCategories */ public void setInvoiceSuspensionCategories(List<InvoiceSuspensionCategory> invoiceSuspensionCategories) { this.invoiceSuspensionCategories = invoiceSuspensionCategories; } /** * @see org.kuali.kfs.module.ar.document.CustomerInvoiceDocument#answerSplitNodeQuestion(java.lang.String) */ @Override public boolean answerSplitNodeQuestion(String nodeName) throws UnsupportedOperationException { if (nodeName.equals(REQUIRES_APPROVAL_SPLIT)) { return isRequiresFundingManagerApproval(); } throw new UnsupportedOperationException("Cannot answer split question for this node you call \"" + nodeName + "\""); } /** * @return true if this CINV should route to the fund managers, false if it should skip */ private boolean isRequiresFundingManagerApproval() { final ContractsGrantsInvoiceDocumentService contractsGrantsInvoiceDocumentService = SpringContext.getBean(ContractsGrantsInvoiceDocumentService.class); // if auto approve on the award is false or suspension exists or the award is auto-approve but fails to pass validation, then we need to have funds manager approve. boolean result; result = !CollectionUtils.isEmpty(getInvoiceSuspensionCategories()) || !getInvoiceGeneralDetail().getAward().getAutoApproveIndicator() || (contractsGrantsInvoiceDocumentService.isDocumentBatchCreated(this) && !contractsGrantsInvoiceDocumentService.doesInvoicePassValidation(this)); return result; } public KualiDecimal getPaymentAmount() { return paymentAmount; } public void setPaymentAmount(KualiDecimal paymentAmount) { this.paymentAmount = paymentAmount; } public KualiDecimal getBalanceDue() { return balanceDue; } public void setBalanceDue(KualiDecimal balanceDue) { this.balanceDue = balanceDue; } public String getCustomerNumber() { return accountsReceivableDocumentHeader.getCustomerNumber(); } public KualiDecimal getTotalBudgetAmount() { KualiDecimal total = KualiDecimal.ZERO; for (InvoiceAccountDetail accountDetail : accountDetails) { total = total.add(accountDetail.getTotalBudget()); } return total; } public KualiDecimal getTotalInvoiceAmount() { KualiDecimal total = KualiDecimal.ZERO; for (InvoiceAccountDetail accountDetail : accountDetails) { total = total.add(accountDetail.getInvoiceAmount()); } return total; } public KualiDecimal getTotalCumulativeExpenditures() { KualiDecimal total = KualiDecimal.ZERO; for (InvoiceAccountDetail accountDetail : accountDetails) { total = total.add(accountDetail.getCumulativeExpenditures()); } return total; } public KualiDecimal getTotalBudgetRemaining() { KualiDecimal total = KualiDecimal.ZERO; for (InvoiceAccountDetail accountDetail : accountDetails) { total = total.add(accountDetail.getBudgetRemaining()); } return total; } public KualiDecimal getTotalAmountBilledToDate() { KualiDecimal total = KualiDecimal.ZERO; for (InvoiceAccountDetail accountDetail : accountDetails) { total = total.add(accountDetail.getTotalAmountBilledToDate()); } return total; } /** * The CINV's rule is that if an invoice detail is positive, it's a debit, and if it's negative, it's a credit * @see org.kuali.kfs.module.ar.document.CustomerInvoiceDocument#isInvoiceDetailReceivableDebit(org.kuali.kfs.module.ar.businessobject.CustomerInvoiceDetail) */ @Override protected boolean isInvoiceDetailReceivableDebit(CustomerInvoiceDetail customerInvoiceDetail) { return customerInvoiceDetail.getAmount().isZero() || customerInvoiceDetail.getAmount().isPositive(); } /** * If the invoice detail is negative, then return debit here, otherwise it's a credit * @see org.kuali.kfs.module.ar.document.CustomerInvoiceDocument#isInvoiceDetailIncomeDebit(org.kuali.kfs.module.ar.businessobject.CustomerInvoiceDetail) */ @Override protected boolean isInvoiceDetailIncomeDebit(CustomerInvoiceDetail customerInvoiceDetail) { return customerInvoiceDetail.getAmount().isZero() || customerInvoiceDetail.getAmount().isNegative(); } }