/* * 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.web.struts; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.kuali.kfs.module.ar.ArKeyConstants; import org.kuali.kfs.module.ar.ArPropertyConstants; import org.kuali.kfs.module.ar.businessobject.AccountsReceivableDocumentHeader; import org.kuali.kfs.module.ar.businessobject.Customer; import org.kuali.kfs.module.ar.businessobject.CustomerInvoiceDetail; import org.kuali.kfs.module.ar.businessobject.InvoicePaidApplied; import org.kuali.kfs.module.ar.businessobject.NonAppliedHolding; import org.kuali.kfs.module.ar.businessobject.NonInvoiced; import org.kuali.kfs.module.ar.document.CustomerInvoiceDocument; import org.kuali.kfs.module.ar.document.PaymentApplicationDocument; import org.kuali.kfs.module.ar.document.service.AccountsReceivableDocumentHeaderService; import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDetailService; import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDocumentService; import org.kuali.kfs.module.ar.document.service.NonAppliedHoldingService; import org.kuali.kfs.module.ar.document.service.PaymentApplicationDocumentService; import org.kuali.kfs.module.ar.document.validation.impl.PaymentApplicationDocumentRuleUtil; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.context.SpringContext; import org.kuali.kfs.sys.document.web.struts.FinancialSystemTransactionalDocumentActionBase; import org.kuali.rice.core.api.util.type.KualiDecimal; import org.kuali.rice.kew.api.exception.WorkflowException; import org.kuali.rice.kns.web.struts.form.KualiDocumentFormBase; import org.kuali.rice.krad.document.Document; import org.kuali.rice.krad.service.BusinessObjectService; import org.kuali.rice.krad.service.DocumentService; import org.kuali.rice.krad.util.GlobalVariables; import org.kuali.rice.krad.util.KRADConstants; import org.kuali.rice.krad.util.ObjectUtils; public class PaymentApplicationDocumentAction extends FinancialSystemTransactionalDocumentActionBase { /** * @see org.kuali.rice.kns.web.struts.action.KualiDocumentActionBase#save(org.apache.struts.action.ActionMapping, * org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ @Override public ActionForward save(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { doApplicationOfFunds((PaymentApplicationDocumentForm) form); return super.save(mapping, form, request, response); } protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PaymentApplicationDocumentAction.class); protected BusinessObjectService businessObjectService; protected DocumentService documentService; protected PaymentApplicationDocumentService paymentApplicationDocumentService; protected CustomerInvoiceDocumentService customerInvoiceDocumentService; protected CustomerInvoiceDetailService customerInvoiceDetailService; protected NonAppliedHoldingService nonAppliedHoldingService; /** * Constructs a PaymentApplicationDocumentAction.java. */ public PaymentApplicationDocumentAction() { super(); businessObjectService = SpringContext.getBean(BusinessObjectService.class); documentService = SpringContext.getBean(DocumentService.class); paymentApplicationDocumentService = SpringContext.getBean(PaymentApplicationDocumentService.class); customerInvoiceDocumentService = SpringContext.getBean(CustomerInvoiceDocumentService.class); customerInvoiceDetailService = SpringContext.getBean(CustomerInvoiceDetailService.class); nonAppliedHoldingService = SpringContext.getBean(NonAppliedHoldingService.class); } /** * @see org.apache.struts.action.Action#execute(org.apache.struts.action.ActionMapping, org.apache.struts.action.ActionForm, * javax.servlet.ServletRequest, javax.servlet.ServletResponse) */ @Override public ActionForward execute(ActionMapping mapping, ActionForm form, ServletRequest request, ServletResponse response) throws Exception { PaymentApplicationDocumentForm payAppForm = (PaymentApplicationDocumentForm) form; if (!payAppForm.getPaymentApplicationDocument().isFinal()) { doApplicationOfFunds(payAppForm); } return super.execute(mapping, form, request, response); } /** * This is overridden in order to recalculate the invoice totals before doing the submit. * * @see org.kuali.rice.kns.web.struts.action.KualiDocumentActionBase#route(org.apache.struts.action.ActionMapping, * org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ @Override public ActionForward route(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { doApplicationOfFunds((PaymentApplicationDocumentForm) form); return super.route(mapping, form, request, response); } /** * @param mapping * @param form * @param request * @param response * @return * @throws Exception */ public ActionForward deleteNonArLine(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { PaymentApplicationDocumentForm paymentApplicationDocumentForm = (PaymentApplicationDocumentForm) form; // TODO Andrew - should this be run or not here? // doApplicationOfFunds((PaymentApplicationDocumentForm)form); PaymentApplicationDocument paymentApplicationDocument = paymentApplicationDocumentForm.getPaymentApplicationDocument(); Map<String, Object> parameters = request.getParameterMap(); String indexToRemoveString = null; Integer indexToRemove = null; // Figure out which line to remove. for (String k : parameters.keySet()) { if (k.startsWith(ArPropertyConstants.PaymentApplicationDocumentFields.DELETE_NON_INVOICED_LINE_PREFIX) && k.endsWith(".x")) { if (null != parameters.get(k)) { int beginIndex = ArPropertyConstants.PaymentApplicationDocumentFields.DELETE_NON_INVOICED_LINE_PREFIX.length(); int endIndex = k.lastIndexOf("."); if (beginIndex >= 0 && endIndex > beginIndex) { indexToRemoveString = k.substring(beginIndex, endIndex); } break; } } } // If we know which line to remove, remove it. if (null != indexToRemoveString) { indexToRemove = new Integer(indexToRemoveString); NonInvoiced toRemove = null; for (NonInvoiced nonInvoiced : paymentApplicationDocument.getNonInvoiceds()) { if (indexToRemove.equals(nonInvoiced.getFinancialDocumentLineNumber())) { toRemove = nonInvoiced; break; } } if (null != toRemove) { paymentApplicationDocument.getNonInvoiceds().remove(toRemove); } } // re-number the non-invoiceds Integer nonInvoicedItemNumber = 1; for (NonInvoiced n : paymentApplicationDocument.getNonInvoiceds()) { n.setFinancialDocumentLineNumber(nonInvoicedItemNumber++); n.setFinancialDocumentLineNumber(nonInvoicedItemNumber++); n.refreshReferenceObject("chartOfAccounts"); n.refreshReferenceObject("account"); n.refreshReferenceObject("subAccount"); n.refreshReferenceObject("financialObject"); n.refreshReferenceObject("financialSubObject"); n.refreshReferenceObject("project"); } return mapping.findForward(KFSConstants.MAPPING_BASIC); } /** * Create an InvoicePaidApplied for a CustomerInvoiceDetail and validate it. If the validation succeeds the paidApplied is * returned. If the validation does succeed a null is returned. * * @param customerInvoiceDetail * @param paymentApplicationDocument * @param amount * @param fieldName * @return * @throws WorkflowException */ protected InvoicePaidApplied generateAndValidateNewPaidApplied(PaymentApplicationInvoiceDetailApply detailApplication, String fieldName, PaymentApplicationDocument document) { // generate the paidApplied InvoicePaidApplied paidApplied = detailApplication.generatePaidApplied(); // validate the paidApplied, but ignore any failures (other than the error message) LOG.debug("Validating the generated paidApplied " + paidApplied.getDocumentNumber()); PaymentApplicationDocumentRuleUtil.validateInvoicePaidApplied(paidApplied, fieldName, document); // return the generated paidApplied return paidApplied; } /** * @param mapping * @param form * @param request * @param response * @return * @throws Exception */ public ActionForward applyAllAmounts(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { doApplicationOfFunds((PaymentApplicationDocumentForm) form); return mapping.findForward(KFSConstants.MAPPING_BASIC); } /** * @param paymentApplicationDocumentForm * @throws WorkflowException */ protected void doApplicationOfFunds(PaymentApplicationDocumentForm paymentApplicationDocumentForm) throws WorkflowException { PaymentApplicationDocument paymentApplicationDocument = paymentApplicationDocumentForm.getPaymentApplicationDocument(); List<InvoicePaidApplied> invoicePaidApplieds = new ArrayList<InvoicePaidApplied>(); // apply invoice detail entries invoicePaidApplieds.addAll(applyToIndividualCustomerInvoiceDetails(paymentApplicationDocumentForm)); // quick-apply invoices invoicePaidApplieds.addAll(quickApplyToInvoices(paymentApplicationDocumentForm, invoicePaidApplieds)); // re-number the paidApplieds internal sequence numbers int paidAppliedItemNumber = 1; for (InvoicePaidApplied i : invoicePaidApplieds) { i.setPaidAppliedItemNumber(paidAppliedItemNumber++); } // apply non-Invoiced NonInvoiced nonInvoiced = applyNonInvoiced(paymentApplicationDocumentForm); // apply non-applied holdings NonAppliedHolding nonAppliedHolding = applyUnapplied(paymentApplicationDocumentForm); // sum up the paid applieds KualiDecimal sumOfInvoicePaidApplieds = KualiDecimal.ZERO; for (InvoicePaidApplied invoicePaidApplied : invoicePaidApplieds) { KualiDecimal amount = invoicePaidApplied.getInvoiceItemAppliedAmount(); if (null == amount) { amount = KualiDecimal.ZERO; } sumOfInvoicePaidApplieds = sumOfInvoicePaidApplieds.add(amount); } // sum up all applieds KualiDecimal appliedAmount = KualiDecimal.ZERO; appliedAmount = appliedAmount.add(sumOfInvoicePaidApplieds); if (null != nonInvoiced && null != nonInvoiced.getFinancialDocumentLineAmount()) { appliedAmount = appliedAmount.add(nonInvoiced.getFinancialDocumentLineAmount()); } appliedAmount = appliedAmount.add(paymentApplicationDocument.getSumOfNonAppliedDistributions()); appliedAmount = appliedAmount.add(paymentApplicationDocument.getSumOfNonInvoicedDistributions()); appliedAmount = appliedAmount.add(paymentApplicationDocument.getSumOfNonInvoiceds()); if (null != paymentApplicationDocument.getNonAppliedHoldingAmount()) { appliedAmount = appliedAmount.add(paymentApplicationDocument.getNonAppliedHoldingAmount()); } // check that we havent applied more than our control total KualiDecimal controlTotalAmount = paymentApplicationDocumentForm.getTotalFromControl(); // if the person over-applies, we dont stop them, we just complain if (appliedAmount.isGreaterThan(controlTotalAmount)) { addGlobalError(ArKeyConstants.PaymentApplicationDocumentErrors.CANNOT_APPLY_MORE_THAN_CASH_CONTROL_TOTAL_AMOUNT); } // swap out the old paidApplieds with the newly generated paymentApplicationDocument.getInvoicePaidApplieds().clear(); paymentApplicationDocument.getInvoicePaidApplieds().addAll(invoicePaidApplieds); // NonInvoiced list management if (null != nonInvoiced) { paymentApplicationDocument.getNonInvoiceds().add(nonInvoiced); // re-number the non-invoiced Integer nonInvoicedItemNumber = 1; for (NonInvoiced n : paymentApplicationDocument.getNonInvoiceds()) { n.setFinancialDocumentLineNumber(nonInvoicedItemNumber++); n.refreshReferenceObject("chartOfAccounts"); n.refreshReferenceObject("account"); n.refreshReferenceObject("subAccount"); n.refreshReferenceObject("financialObject"); n.refreshReferenceObject("financialSubObject"); n.refreshReferenceObject("project"); } // make an empty new one paymentApplicationDocumentForm.setNonInvoicedAddLine(new NonInvoiced()); } // reset the allocations, so it gets re-calculated paymentApplicationDocumentForm.setNonAppliedControlAllocations(null); // Update the doc total if it is not a CashControl generated PayApp if (!paymentApplicationDocument.hasCashControlDetail()) { paymentApplicationDocument.getFinancialSystemDocumentHeader().setFinancialDocumentTotalAmount(appliedAmount); } } /** * @param paymentApplicationDocumentForm * @return */ protected List<InvoicePaidApplied> applyToIndividualCustomerInvoiceDetails(PaymentApplicationDocumentForm paymentApplicationDocumentForm) { PaymentApplicationDocument paymentApplicationDocument = paymentApplicationDocumentForm.getPaymentApplicationDocument(); String applicationDocNbr = paymentApplicationDocument.getDocumentNumber(); // Handle amounts applied at the invoice detail level int paidAppliedsGenerated = 1; int simpleInvoiceDetailApplicationCounter = 0; // calculate paid applieds for all invoices List<InvoicePaidApplied> invoicePaidApplieds = this.filterTempInvoicePaidApplieds(paymentApplicationDocumentForm); for (PaymentApplicationInvoiceApply invoiceApplication : paymentApplicationDocumentForm.getInvoiceApplications()) { for (PaymentApplicationInvoiceDetailApply detailApplication : invoiceApplication.getDetailApplications()) { // selectedInvoiceDetailApplications[${ctr}].amountApplied String fieldName = "selectedInvoiceDetailApplications[" + Integer.toString(simpleInvoiceDetailApplicationCounter) + "].amountApplied"; simpleInvoiceDetailApplicationCounter++; // needs to be incremented even if we skip this line // handle the user clicking full apply if (detailApplication.isFullApply()) { detailApplication.setAmountApplied(detailApplication.getAmountOpen()); } // handle the user manually entering an amount else { if (detailApplication.isFullApplyChanged()) { // means it went from true to false detailApplication.setAmountApplied(KualiDecimal.ZERO); } } // Don't add lines where the amount to apply is zero. Wouldn't make any sense to do that. if (KualiDecimal.ZERO.equals(detailApplication.getAmountApplied())) { continue; } if (containsIdentical(detailApplication.getInvoiceDetail(), detailApplication.getAmountApplied(), invoicePaidApplieds)) { continue; } // generate and validate the paidApplied, and always add it to the list, even if // it fails validation. Validation failures will stop routing. LOG.debug("Generating paid applied for detail application " + detailApplication.getInvoiceDocumentNumber()); InvoicePaidApplied invoicePaidApplied = generateAndValidateNewPaidApplied(detailApplication, fieldName, paymentApplicationDocument); GlobalVariables.getMessageMap().addToErrorPath(KFSConstants.PaymentApplicationTabErrorCodes.APPLY_TO_INVOICE_DETAIL_TAB); GlobalVariables.getMessageMap().removeFromErrorPath(KFSConstants.PaymentApplicationTabErrorCodes.APPLY_TO_INVOICE_DETAIL_TAB); invoicePaidApplieds.add(invoicePaidApplied); paidAppliedsGenerated++; } } return invoicePaidApplieds; } /** * @param paymentApplicationDocumentForm * @param appliedToIndividualDetails * @return */ protected List<InvoicePaidApplied> filterTempInvoicePaidApplieds(PaymentApplicationDocumentForm paymentApplicationDocumentForm) { PaymentApplicationDocument paymentApplicationDocument = paymentApplicationDocumentForm.getPaymentApplicationDocument(); List <InvoicePaidApplied> filteredInvoicePaidApplieds = new ArrayList <InvoicePaidApplied>(); List<InvoicePaidApplied> invoicePaidApplieds = paymentApplicationDocument.getInvoicePaidApplieds(); // jira fix // add only entries that do not have the loaded customer number String currentCustomerNumber = findCustomerNumber(paymentApplicationDocumentForm); for (InvoicePaidApplied invoicePaidApplied : invoicePaidApplieds) { filteredInvoicePaidApplieds.add(invoicePaidApplied); } return filteredInvoicePaidApplieds; } /** * figure out the current customer Number on the form * @param paymentApplicationDocumentForm * @return */ protected String findCustomerNumber(PaymentApplicationDocumentForm paymentApplicationDocumentForm) { boolean validInvoice = this.isValidInvoice(paymentApplicationDocumentForm); String customerNumber = paymentApplicationDocumentForm.getSelectedCustomerNumber(); String currentInvoiceNumber = paymentApplicationDocumentForm.getEnteredInvoiceDocumentNumber(); // Invoice number entered, but no customer number entered if (StringUtils.isBlank(customerNumber) && StringUtils.isNotBlank(currentInvoiceNumber) && validInvoice) { Customer customer = customerInvoiceDocumentService.getCustomerByInvoiceDocumentNumber(currentInvoiceNumber); customerNumber = customer.getCustomerNumber(); } return customerNumber; } /** * checks if the invoice is valid * @param paymentApplicationDocumentForm * @return */ protected boolean isValidInvoice(PaymentApplicationDocumentForm paymentApplicationDocumentForm) { boolean validInvoice = true; if (StringUtils.isNotBlank(paymentApplicationDocumentForm.getEnteredInvoiceDocumentNumber())) { Map<String, String> pkMap = new HashMap<String, String>(); if (!SpringContext.getBean(CustomerInvoiceDocumentService.class).checkIfInvoiceNumberIsFinal(paymentApplicationDocumentForm.getEnteredInvoiceDocumentNumber())) { validInvoice &= false; } } return validInvoice; } /* * test if this is already been applied * InvoicePaidApplied paidApplied = new InvoicePaidApplied(payAppDocNumber, invoiceDetail.getDocumentNumber(), invoiceDetail.getSequenceNumber(), amountApplied, DEFAULT_PAID_APPLIED_ITEM_NUMBER); String documentNumber, String refInvoiceDocNumber, Integer invoiceSequenceNumber, KualiDecimal appliedAmount, Integer paidAppliedItemNumber) { */ protected boolean containsIdentical(CustomerInvoiceDetail customerInvoiceDetail, KualiDecimal amountApplied, List<InvoicePaidApplied> invoicePaidApplieds ) { boolean identicalFlag = false; String custRefInvoiceDocNumber = customerInvoiceDetail.getDocumentNumber(); Integer custInvoiceSequenceNumber = customerInvoiceDetail.getInvoiceItemNumber(); for (InvoicePaidApplied invoicePaidApplied : invoicePaidApplieds) { String payAppDocNumber = invoicePaidApplied.getDocumentNumber(); String refInvoiceDocNumber = invoicePaidApplied.getFinancialDocumentReferenceInvoiceNumber(); Integer invoiceSequenceNumber = invoicePaidApplied.getInvoiceItemNumber(); KualiDecimal appliedAmount = invoicePaidApplied.getInvoiceItemAppliedAmount(); Integer paidAppliedItemNumber = invoicePaidApplied.getPaidAppliedItemNumber(); if (custRefInvoiceDocNumber.equals(refInvoiceDocNumber) && custInvoiceSequenceNumber.equals(invoiceSequenceNumber) && amountApplied.equals(appliedAmount)) { identicalFlag = true; break; } } return identicalFlag; } protected List<InvoicePaidApplied> quickApplyToInvoices(PaymentApplicationDocumentForm paymentApplicationDocumentForm, List<InvoicePaidApplied> appliedToIndividualDetails) { PaymentApplicationDocument applicationDocument = (PaymentApplicationDocument) paymentApplicationDocumentForm.getDocument(); List<InvoicePaidApplied> invoicePaidApplieds = new ArrayList<InvoicePaidApplied>(); // go over the selected invoices and apply full amount to each of their details int index = 0; for (PaymentApplicationInvoiceApply invoiceApplication : paymentApplicationDocumentForm.getInvoiceApplications()) { String invoiceDocNumber = invoiceApplication.getDocumentNumber(); // skip the line if its not set to quick apply if (!invoiceApplication.isQuickApply()) { // if it was just flipped from True to False if (invoiceApplication.isQuickApplyChanged()) { for (PaymentApplicationInvoiceDetailApply detailApplication : invoiceApplication.getDetailApplications()) { // zero out all the details detailApplication.setAmountApplied(KualiDecimal.ZERO); detailApplication.setFullApply(false); // remove any existing paidApplieds for this invoice for (int i = appliedToIndividualDetails.size() - 1; i >= 0; i--) { InvoicePaidApplied applied = appliedToIndividualDetails.get(i); if (applied.getFinancialDocumentReferenceInvoiceNumber().equals(invoiceApplication.getDocumentNumber())) { appliedToIndividualDetails.remove(i); } } } } continue; } // make sure none of the invoices selected have zero open amounts, complain if so if (invoiceApplication.getOpenAmount().isZero()) { addGlobalError(ArKeyConstants.PaymentApplicationDocumentErrors.CANNOT_QUICK_APPLY_ON_INVOICE_WITH_ZERO_OPEN_AMOUNT); return invoicePaidApplieds; } // remove any existing paidApplieds for this invoice for (int i = appliedToIndividualDetails.size() - 1; i >= 0; i--) { InvoicePaidApplied applied = appliedToIndividualDetails.get(i); if (applied.getFinancialDocumentReferenceInvoiceNumber().equals(invoiceApplication.getDocumentNumber())) { appliedToIndividualDetails.remove(i); } } // create and validate the paid applieds for each invoice detail String fieldName = "invoiceApplications[" + invoiceDocNumber + "].quickApply"; for (PaymentApplicationInvoiceDetailApply detailApplication : invoiceApplication.getDetailApplications()) { detailApplication.setAmountApplied(detailApplication.getAmountOpen()); detailApplication.setFullApply(true); InvoicePaidApplied paidApplied = generateAndValidateNewPaidApplied(detailApplication, fieldName, applicationDocument); if (paidApplied != null) { invoicePaidApplieds.add(paidApplied); } } // maintain the selected doc number if (invoiceDocNumber.equals(paymentApplicationDocumentForm.getEnteredInvoiceDocumentNumber())) { paymentApplicationDocumentForm.setSelectedInvoiceDocumentNumber(invoiceDocNumber); } } return invoicePaidApplieds; } /** * @param payAppForm * @return * @throws WorkflowException */ protected NonInvoiced applyNonInvoiced(PaymentApplicationDocumentForm payAppForm) throws WorkflowException { PaymentApplicationDocument applicationDocument = (PaymentApplicationDocument) payAppForm.getDocument(); NonInvoiced nonInvoiced = payAppForm.getNonInvoicedAddLine(); // if the line or line amount is null or zero, don't add the line. Additional validation is performed for the amount within // the rules // class, so no validation is needed here. // // NOTE: This conditional is in place because the "apply" button on the payment application document functions as a // universal button, // and therefore checks each tab where the button resides on the interface and attempts to apply values for that tab. This // functionality // causes this method to be called, regardless of if any values were entered in the "Non-AR" tab of the document. We want to // ignore this // method being called if there are no values entered in the fields. // // For the sake of this algorithm, a "Non-AR" accounting line will be ignored if it is null, or if the dollar amount entered // is blank or zero. if (ObjectUtils.isNull(payAppForm.getNonInvoicedAddLine()) || nonInvoiced.getFinancialDocumentLineAmount() == null || nonInvoiced.getFinancialDocumentLineAmount().isZero()) { return null; } // If we got past the above conditional, wire it up for adding nonInvoiced.setFinancialDocumentPostingYear(applicationDocument.getPostingYear()); nonInvoiced.setDocumentNumber(applicationDocument.getDocumentNumber()); nonInvoiced.setFinancialDocumentLineNumber(payAppForm.getNextNonInvoicedLineNumber()); if (StringUtils.isNotBlank(nonInvoiced.getChartOfAccountsCode())) { nonInvoiced.setChartOfAccountsCode(nonInvoiced.getChartOfAccountsCode().toUpperCase()); } // run the validations boolean isValid = PaymentApplicationDocumentRuleUtil.validateNonInvoiced(nonInvoiced, applicationDocument, payAppForm.getTotalFromControl()); // check the validation results and return null if there were any errors if (!isValid) { return null; } return nonInvoiced; } /** * @param payAppForm * @return * @throws WorkflowException */ protected NonAppliedHolding applyUnapplied(PaymentApplicationDocumentForm payAppForm) throws WorkflowException { PaymentApplicationDocument payAppDoc = payAppForm.getPaymentApplicationDocument(); KualiDecimal amount = payAppForm.getNonAppliedHoldingAmount(); // validate the customer number in the unapplied if (StringUtils.isNotBlank(payAppForm.getNonAppliedHoldingCustomerNumber())) { Map<String, String> pkMap = new HashMap<String, String>(); pkMap.put(ArPropertyConstants.CustomerFields.CUSTOMER_NUMBER, payAppForm.getNonAppliedHoldingCustomerNumber().toUpperCase()); int found = businessObjectService.countMatching(Customer.class, pkMap); if (found == 0) { addFieldError(KFSConstants.PaymentApplicationTabErrorCodes.UNAPPLIED_TAB, ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_CUSTOMER_NUMBER, ArKeyConstants.PaymentApplicationDocumentErrors.ENTERED_INVOICE_CUSTOMER_NUMBER_INVALID); return null; } // force customer number to upper payAppForm.setNonAppliedHoldingCustomerNumber(payAppForm.getNonAppliedHoldingCustomerNumber().toUpperCase()); } // validate the amount in the unapplied if (payAppForm.getNonAppliedHoldingAmount() != null && payAppForm.getNonAppliedHoldingAmount().isNegative()) { addFieldError(KFSConstants.PaymentApplicationTabErrorCodes.UNAPPLIED_TAB, ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_AMOUNT, ArKeyConstants.PaymentApplicationDocumentErrors.UNAPPLIED_AMOUNT_CANNOT_BE_NEGATIVE); return null; } // if we dont have enough information to make an UnApplied, then do nothing if (StringUtils.isBlank(payAppForm.getNonAppliedHoldingCustomerNumber()) || amount == null || amount.isZero()) { payAppDoc.setNonAppliedHolding(null); return null; } // build a new NonAppliedHolding NonAppliedHolding nonAppliedHolding = new NonAppliedHolding(); nonAppliedHolding.setCustomerNumber(payAppForm.getNonAppliedHoldingCustomerNumber().toUpperCase()); nonAppliedHolding.setReferenceFinancialDocumentNumber(payAppDoc.getDocumentNumber()); nonAppliedHolding.setFinancialDocumentLineAmount(amount); // set it to the document payAppDoc.setNonAppliedHolding(nonAppliedHolding); // validate it boolean isValid = PaymentApplicationDocumentRuleUtil.validateNonAppliedHolding(payAppDoc, payAppForm.getTotalFromControl()); // check the validation results and return null if there were any errors if (!isValid) { return null; } return nonAppliedHolding; } /** * This method loads the invoices for currently selected customer * * @param applicationDocumentForm */ protected void loadInvoices(PaymentApplicationDocumentForm payAppForm, String selectedInvoiceNumber) { PaymentApplicationDocument payAppDoc = payAppForm.getPaymentApplicationDocument(); AccountsReceivableDocumentHeader arDocHeader = payAppDoc.getAccountsReceivableDocumentHeader(); String currentInvoiceNumber = selectedInvoiceNumber; // before we do anything, validate the validity of any customerNumber or invoiceNumber // entered against the db, and complain to the user if either is not right. if (StringUtils.isNotBlank(payAppForm.getSelectedCustomerNumber())) { Map<String, String> pkMap = new HashMap<String, String>(); pkMap.put(ArPropertyConstants.CustomerFields.CUSTOMER_NUMBER, payAppForm.getSelectedCustomerNumber()); int found = businessObjectService.countMatching(Customer.class, pkMap); if (found == 0) { addFieldError(KFSConstants.PaymentApplicationTabErrorCodes.APPLY_TO_INVOICE_DETAIL_TAB, ArPropertyConstants.PaymentApplicationDocumentFields.ENTERED_INVOICE_CUSTOMER_NUMBER, ArKeyConstants.PaymentApplicationDocumentErrors.ENTERED_INVOICE_CUSTOMER_NUMBER_INVALID); } } boolean validInvoice = this.isValidInvoice(payAppForm); if (!validInvoice) { addFieldError(KFSConstants.PaymentApplicationTabErrorCodes.APPLY_TO_INVOICE_DETAIL_TAB, ArPropertyConstants.PaymentApplicationDocumentFields.ENTERED_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_INVOICE_DOCUMENT_NOT_FINAL); } // This handles the priority of the payapp selected customer number and the // ar doc header customer number. The ar doc header customer number should always // reflect what customer number is entered on the form for invoices. This code chunk // ensures that whatever the user enters always wins, but also tries to not load the form // with an empty customer number wherever possible. if (StringUtils.isBlank(payAppForm.getSelectedCustomerNumber())) { if (StringUtils.isBlank(arDocHeader.getCustomerNumber())) { if (payAppDoc.hasCashControlDetail()) { payAppForm.setSelectedCustomerNumber(payAppDoc.getCashControlDetail().getCustomerNumber()); arDocHeader.setCustomerNumber(payAppDoc.getCashControlDetail().getCustomerNumber()); } } else { payAppForm.setSelectedCustomerNumber(arDocHeader.getCustomerNumber()); } } else { arDocHeader.setCustomerNumber(payAppForm.getSelectedCustomerNumber()); } String customerNumber = payAppForm.getSelectedCustomerNumber(); // Invoice number entered, but no customer number entered if (StringUtils.isBlank(customerNumber) && StringUtils.isNotBlank(currentInvoiceNumber) && validInvoice) { Customer customer = customerInvoiceDocumentService.getCustomerByInvoiceDocumentNumber(currentInvoiceNumber); customerNumber = customer.getCustomerNumber(); payAppDoc.getAccountsReceivableDocumentHeader().setCustomerNumber(customerNumber); } // load up the control docs and non-applied holdings for non-cash-control payapps if (StringUtils.isNotBlank(customerNumber)) { if (!payAppDoc.hasCashControlDocument()) { List<PaymentApplicationDocument> nonAppliedControlDocs = new ArrayList<PaymentApplicationDocument>(); List<NonAppliedHolding> nonAppliedControlHoldings = new ArrayList<NonAppliedHolding>(); // if the doc is already final/approved, then we only pull the relevant control // documents and nonapplied holdings that this doc paid against. if (payAppDoc.isFinal()) { nonAppliedControlDocs.addAll(payAppDoc.getPaymentApplicationDocumentsUsedAsControlDocuments()); nonAppliedControlHoldings.addAll(payAppDoc.getNonAppliedHoldingsUsedAsControls()); } // otherwise, we pull all available non-zero non-applied holdings for // this customer, and make the associated docs and non-applied holdings available else { // retrieve the set of available non-applied holdings for this customer NonAppliedHoldingService nonAppliedHoldingService = SpringContext.getBean(NonAppliedHoldingService.class); nonAppliedControlHoldings.addAll(nonAppliedHoldingService.getNonAppliedHoldingsForCustomer(customerNumber)); // get the parent list of payapp documents that they come from List<String> controlDocNumbers = new ArrayList<String>(); for (NonAppliedHolding nonAppliedHolding : nonAppliedControlHoldings) { if (nonAppliedHolding.getAvailableUnappliedAmount().isPositive()) { if (!controlDocNumbers.contains(nonAppliedHolding.getReferenceFinancialDocumentNumber())) { controlDocNumbers.add(nonAppliedHolding.getReferenceFinancialDocumentNumber()); } } } // only try to retrieve docs if we have any to retrieve if (!controlDocNumbers.isEmpty()) { try { List<Document> docs = documentService.getDocumentsByListOfDocumentHeaderIds(PaymentApplicationDocument.class, controlDocNumbers); for (Document doc : docs) { nonAppliedControlDocs.add((PaymentApplicationDocument)doc); } } catch (WorkflowException e) { throw new RuntimeException("A runtimeException was thrown when trying to retrieve a list of documents.", e); } } } // set the form vars from what we've loaded up here payAppForm.setNonAppliedControlDocs(nonAppliedControlDocs); payAppForm.setNonAppliedControlHoldings(nonAppliedControlHoldings); payAppDoc.setNonAppliedHoldingsForCustomer(new ArrayList<NonAppliedHolding>(nonAppliedControlHoldings)); payAppForm.setNonAppliedControlAllocations(null); } } // reload invoices for the selected customer number if (StringUtils.isNotBlank(customerNumber)) { Collection<CustomerInvoiceDocument> openInvoicesForCustomer; // we have to special case the invoices once the document is finished, because // at this point, we want to show the invoices it paid against, NOT the set of // open invoices if (payAppDoc.isFinal()) { openInvoicesForCustomer = payAppDoc.getInvoicesPaidAgainst(); } else { openInvoicesForCustomer = customerInvoiceDocumentService.getOpenInvoiceDocumentsByCustomerNumber(customerNumber); } payAppForm.setInvoices(new ArrayList<CustomerInvoiceDocument>(openInvoicesForCustomer)); payAppForm.setupInvoiceWrappers(payAppDoc.getDocumentNumber()); } // if no invoice number entered than get the first invoice if (StringUtils.isNotBlank(customerNumber) && StringUtils.isBlank(currentInvoiceNumber)) { if (payAppForm.getInvoices() == null || payAppForm.getInvoices().isEmpty()) { currentInvoiceNumber = null; } else { currentInvoiceNumber = payAppForm.getInvoices().get(0).getDocumentNumber(); } } // load information for the current selected invoice if (StringUtils.isNotBlank(currentInvoiceNumber)) { payAppForm.setSelectedInvoiceDocumentNumber(currentInvoiceNumber); payAppForm.setEnteredInvoiceDocumentNumber(currentInvoiceNumber); } // make sure all paidApplieds are synched with the PaymentApplicationInvoiceApply and // PaymentApplicationInvoiceDetailApply objects, so that the form reflects how it was left pre-save. // This is only necessary when the doc is saved, and then re-opened, as the invoice-detail wrappers // will no longer hold the state info. I know this is a monstrosity. Get over it. for (InvoicePaidApplied paidApplied : payAppDoc.getInvoicePaidApplieds()) { for (PaymentApplicationInvoiceApply invoiceApplication : payAppForm.getInvoiceApplications()) { if (paidApplied.getFinancialDocumentReferenceInvoiceNumber().equalsIgnoreCase(invoiceApplication.getDocumentNumber())) { for (PaymentApplicationInvoiceDetailApply detailApplication : invoiceApplication.getDetailApplications()) { if (paidApplied.getInvoiceItemNumber().equals(detailApplication.getSequenceNumber())) { // if the amount applieds dont match, then have the paidApplied fill in the applied amounts // for the invoiceApplication details if (!paidApplied.getInvoiceItemAppliedAmount().equals(detailApplication.getAmountApplied())) { detailApplication.setAmountApplied(paidApplied.getInvoiceItemAppliedAmount()); if (paidApplied.getInvoiceItemAppliedAmount().equals(detailApplication.getAmountOpen())) { detailApplication.setFullApply(true); } } } } } } } // clear any NonInvoiced add line information from the form vars payAppForm.setNonInvoicedAddLine(null); // load any NonAppliedHolding information into the form vars if (payAppDoc.getNonAppliedHolding() != null) { payAppForm.setNonAppliedHoldingCustomerNumber(payAppDoc.getNonAppliedHolding().getCustomerNumber()); payAppForm.setNonAppliedHoldingAmount(payAppDoc.getNonAppliedHolding().getFinancialDocumentLineAmount()); } else { // clear any NonAppliedHolding information from the form vars if it's empty payAppForm.setNonAppliedHoldingCustomerNumber(null); payAppForm.setNonAppliedHoldingAmount(null); } //Presort this list to not reload in the jsp - https://jira.kuali.org/browse/KFSCNTRB-1377 payAppForm.setInvoiceApplications(sortInvoiceApplications(payAppForm.getInvoiceApplications())); } protected List<PaymentApplicationInvoiceApply> sortInvoiceApplications(List<PaymentApplicationInvoiceApply> invoiceApplications){ EntryHolderComparator entryHolderComparator = new EntryHolderComparator(); List <EntryHolder> entryHoldings = new ArrayList<EntryHolder>(); for (PaymentApplicationInvoiceApply paymentApplicationInvoiceApply : invoiceApplications){ entryHoldings.add(new EntryHolder(paymentApplicationInvoiceApply.getInvoice().getDocumentHeader().getWorkflowDocument().getDateCreated().toDate(), paymentApplicationInvoiceApply)); } if (entryHoldings.size() > 0) { Collections.sort(entryHoldings, entryHolderComparator); } List <PaymentApplicationInvoiceApply> results = new ArrayList<PaymentApplicationInvoiceApply>(); for (EntryHolder entryHolder : entryHoldings) { results.add((PaymentApplicationInvoiceApply) entryHolder.getHolder()); } return results; } /** * An inner class to point to a specific entry in a group */ protected class EntryHolder { private Date date; private Object holder; /** * Constructs a NonAppliedHolding.EntryHolder * @param NonAppliedHolding the entry to point to * @param Date of doc */ public EntryHolder(Date date, Object holder) { this.date = date; this.holder = holder; } public Date getDate() { return this.date; } public Object getHolder() { return this.holder; } } /** * This comparator is used internally for sorting the list of invoices */ protected static class EntryHolderComparator implements Comparator<EntryHolder> { /** * Compares two Objects based on their creation date * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) */ @Override public int compare(EntryHolder rosencrantz, EntryHolder guildenstern) { return rosencrantz.getDate().compareTo(guildenstern.getDate()); } } /** * This method updates the customer invoice details when a new invoice is selected * * @param mapping * @param form * @param request * @param response * @return * @throws Exception */ public ActionForward goToInvoice(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { PaymentApplicationDocumentForm payAppForm = (PaymentApplicationDocumentForm) form; loadInvoices(payAppForm, payAppForm.getSelectedInvoiceDocumentNumber()); if (!payAppForm.getPaymentApplicationDocument().isFinal()) { doApplicationOfFunds(payAppForm); } return mapping.findForward(KFSConstants.MAPPING_BASIC); } /** * This method updates customer invoice details when next invoice is selected * * @param mapping * @param form * @param request * @param response * @return * @throws Exception */ public ActionForward goToNextInvoice(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { PaymentApplicationDocumentForm payAppForm = (PaymentApplicationDocumentForm) form; loadInvoices(payAppForm, payAppForm.getNextInvoiceDocumentNumber()); if (!payAppForm.getPaymentApplicationDocument().isFinal()) { doApplicationOfFunds(payAppForm); } return mapping.findForward(KFSConstants.MAPPING_BASIC); } /** * This method updates customer invoice details when previous invoice is selected * * @param mapping * @param form * @param request * @param response * @return * @throws Exception */ public ActionForward goToPreviousInvoice(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { PaymentApplicationDocumentForm payAppForm = (PaymentApplicationDocumentForm) form; loadInvoices(payAppForm, payAppForm.getPreviousInvoiceDocumentNumber()); if (!payAppForm.getPaymentApplicationDocument().isFinal()) { doApplicationOfFunds(payAppForm); } return mapping.findForward(KFSConstants.MAPPING_BASIC); } /** * Retrieve all invoices for the selected customer. * * @param mapping * @param form * @param request * @param response * @return * @throws Exception */ public ActionForward loadInvoices(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { PaymentApplicationDocumentForm pform = (PaymentApplicationDocumentForm) form; loadInvoices(pform, pform.getEnteredInvoiceDocumentNumber()); return mapping.findForward(KFSConstants.MAPPING_BASIC); } /** * Cancel the document. * * @see org.kuali.rice.kns.web.struts.action.KualiDocumentActionBase#cancel(org.apache.struts.action.ActionMapping, * org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ @Override public ActionForward cancel(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception { PaymentApplicationDocumentForm newForm = (PaymentApplicationDocumentForm) form; if (null == newForm.getCashControlDocument()) { return super.cancel(mapping, form, request, response); } return mapping.findForward(KFSConstants.MAPPING_BASIC); } /** * @see org.kuali.rice.kns.web.struts.action.KualiDocumentActionBase#createDocument(org.kuali.rice.kns.web.struts.form.KualiDocumentFormBase) */ @Override protected void createDocument(KualiDocumentFormBase kualiDocumentFormBase) throws WorkflowException { super.createDocument(kualiDocumentFormBase); PaymentApplicationDocumentForm form = (PaymentApplicationDocumentForm) kualiDocumentFormBase; PaymentApplicationDocument document = form.getPaymentApplicationDocument(); // create new accounts receivable header and set it to the payment application document AccountsReceivableDocumentHeaderService accountsReceivableDocumentHeaderService = SpringContext.getBean(AccountsReceivableDocumentHeaderService.class); AccountsReceivableDocumentHeader accountsReceivableDocumentHeader = accountsReceivableDocumentHeaderService.getNewAccountsReceivableDocumentHeaderForCurrentUser(); accountsReceivableDocumentHeader.setDocumentNumber(document.getDocumentNumber()); document.setAccountsReceivableDocumentHeader(accountsReceivableDocumentHeader); } /** * @see org.kuali.rice.kns.web.struts.action.KualiDocumentActionBase#loadDocument(org.kuali.rice.kns.web.struts.form.KualiDocumentFormBase) */ @Override protected void loadDocument(KualiDocumentFormBase kualiDocumentFormBase) throws WorkflowException { super.loadDocument(kualiDocumentFormBase); PaymentApplicationDocumentForm pform = (PaymentApplicationDocumentForm) kualiDocumentFormBase; loadInvoices(pform, pform.getEnteredInvoiceDocumentNumber()); } /** * Get an error to display in the UI for a certain field. * * @param propertyName * @param errorKey */ protected void addFieldError(String errorPathToAdd, String propertyName, String errorKey) { GlobalVariables.getMessageMap().addToErrorPath(errorPathToAdd); GlobalVariables.getMessageMap().putError(propertyName, errorKey); GlobalVariables.getMessageMap().removeFromErrorPath(errorPathToAdd); } /** * Get an error to display at the global level, for the whole document. * * @param errorKey */ protected void addGlobalError(String errorKey) { GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KRADConstants.DOCUMENT_ERRORS, errorKey, "document.hiddenFieldForErrors"); } }