001    /*
002     * Copyright 2011 The Kuali Foundation.
003     * 
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     * 
008     * http://www.opensource.org/licenses/ecl2.php
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.kfs.module.ar.document.web.struts;
017    
018    import java.util.ArrayList;
019    import java.util.Collection;
020    import java.util.HashMap;
021    import java.util.List;
022    import java.util.Map;
023    
024    import javax.servlet.ServletRequest;
025    import javax.servlet.ServletResponse;
026    import javax.servlet.http.HttpServletRequest;
027    import javax.servlet.http.HttpServletResponse;
028    
029    import org.apache.commons.lang.StringUtils;
030    import org.apache.struts.action.ActionForm;
031    import org.apache.struts.action.ActionForward;
032    import org.apache.struts.action.ActionMapping;
033    import org.kuali.kfs.module.ar.ArKeyConstants;
034    import org.kuali.kfs.module.ar.ArPropertyConstants;
035    import org.kuali.kfs.module.ar.businessobject.AccountsReceivableDocumentHeader;
036    import org.kuali.kfs.module.ar.businessobject.Customer;
037    import org.kuali.kfs.module.ar.businessobject.InvoicePaidApplied;
038    import org.kuali.kfs.module.ar.businessobject.NonAppliedHolding;
039    import org.kuali.kfs.module.ar.businessobject.NonInvoiced;
040    import org.kuali.kfs.module.ar.document.CustomerInvoiceDocument;
041    import org.kuali.kfs.module.ar.document.PaymentApplicationDocument;
042    import org.kuali.kfs.module.ar.document.service.AccountsReceivableDocumentHeaderService;
043    import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDetailService;
044    import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDocumentService;
045    import org.kuali.kfs.module.ar.document.service.NonAppliedHoldingService;
046    import org.kuali.kfs.module.ar.document.service.PaymentApplicationDocumentService;
047    import org.kuali.kfs.module.ar.document.validation.impl.PaymentApplicationDocumentRuleUtil;
048    import org.kuali.kfs.sys.KFSConstants;
049    import org.kuali.kfs.sys.context.SpringContext;
050    import org.kuali.kfs.sys.document.web.struts.FinancialSystemTransactionalDocumentActionBase;
051    import org.kuali.rice.kew.exception.WorkflowException;
052    import org.kuali.rice.kns.document.Document;
053    import org.kuali.rice.kns.service.BusinessObjectService;
054    import org.kuali.rice.kns.service.DocumentService;
055    import org.kuali.rice.kns.util.GlobalVariables;
056    import org.kuali.rice.kns.util.KNSConstants;
057    import org.kuali.rice.kns.util.KualiDecimal;
058    import org.kuali.rice.kns.util.ObjectUtils;
059    import org.kuali.rice.kns.web.struts.form.KualiDocumentFormBase;
060    import org.kuali.rice.kns.workflow.service.KualiWorkflowDocument;
061    import org.kuali.rice.kns.workflow.service.WorkflowDocumentService;
062    
063    public class PaymentApplicationDocumentAction extends FinancialSystemTransactionalDocumentActionBase {
064    
065        @Override
066        public ActionForward save(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
067            doApplicationOfFunds((PaymentApplicationDocumentForm) form);
068            return super.save(mapping, form, request, response);
069        }
070    
071        protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PaymentApplicationDocumentAction.class);
072    
073        protected BusinessObjectService businessObjectService;
074        protected DocumentService documentService;
075        protected WorkflowDocumentService workflowDocumentService;
076        protected PaymentApplicationDocumentService paymentApplicationDocumentService;
077        protected CustomerInvoiceDocumentService customerInvoiceDocumentService;
078        protected CustomerInvoiceDetailService customerInvoiceDetailService;
079        protected NonAppliedHoldingService nonAppliedHoldingService;
080    
081        /**
082         * Constructs a PaymentApplicationDocumentAction.java.
083         */
084        public PaymentApplicationDocumentAction() {
085            super();
086            businessObjectService = SpringContext.getBean(BusinessObjectService.class);
087            documentService = SpringContext.getBean(DocumentService.class);
088            workflowDocumentService = SpringContext.getBean(WorkflowDocumentService.class);
089            paymentApplicationDocumentService = SpringContext.getBean(PaymentApplicationDocumentService.class);
090            customerInvoiceDocumentService = SpringContext.getBean(CustomerInvoiceDocumentService.class);
091            customerInvoiceDetailService = SpringContext.getBean(CustomerInvoiceDetailService.class);
092            nonAppliedHoldingService = SpringContext.getBean(NonAppliedHoldingService.class);
093        }
094    
095        @Override
096        public ActionForward execute(ActionMapping mapping, ActionForm form, ServletRequest request, ServletResponse response) throws Exception {
097            PaymentApplicationDocumentForm payAppForm = (PaymentApplicationDocumentForm) form;
098            if (!payAppForm.getPaymentApplicationDocument().isFinal()) {
099                doApplicationOfFunds(payAppForm);
100            }
101            return super.execute(mapping, form, request, response);
102        }
103    
104        /**
105         * This is overridden in order to recalculate the invoice totals before doing the submit.
106         * 
107         * @see org.kuali.rice.kns.web.struts.action.KualiDocumentActionBase#route(org.apache.struts.action.ActionMapping,
108         *      org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
109         */
110        @Override
111        public ActionForward route(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
112            doApplicationOfFunds((PaymentApplicationDocumentForm) form);
113            return super.route(mapping, form, request, response);
114        }
115    
116        public ActionForward deleteNonArLine(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
117            PaymentApplicationDocumentForm paymentApplicationDocumentForm = (PaymentApplicationDocumentForm) form;
118    
119            // TODO Andrew - should this be run or not here?
120            // doApplicationOfFunds((PaymentApplicationDocumentForm)form);
121    
122            PaymentApplicationDocument paymentApplicationDocument = paymentApplicationDocumentForm.getPaymentApplicationDocument();
123            Map<String, Object> parameters = request.getParameterMap();
124            String _indexToRemove = null;
125            Integer indexToRemove = null;
126    
127            // Figure out which line to remove.
128            for (String k : parameters.keySet()) {
129                if (k.startsWith(ArPropertyConstants.PaymentApplicationDocumentFields.DELETE_NON_INVOICED_LINE_PREFIX) && k.endsWith(".x")) {
130                    if (null != parameters.get(k)) {
131                        int beginIndex = ArPropertyConstants.PaymentApplicationDocumentFields.DELETE_NON_INVOICED_LINE_PREFIX.length();
132                        int endIndex = k.lastIndexOf(".");
133                        if (beginIndex >= 0 && endIndex > beginIndex) {
134                            _indexToRemove = k.substring(beginIndex, endIndex);
135                        }
136                        break;
137                    }
138                }
139            }
140    
141            // If we know which line to remove, remove it.
142            if (null != _indexToRemove) {
143                indexToRemove = new Integer(_indexToRemove);
144                NonInvoiced toRemove = null;
145                for (NonInvoiced nonInvoiced : paymentApplicationDocument.getNonInvoiceds()) {
146                    if (indexToRemove.equals(nonInvoiced.getFinancialDocumentLineNumber())) {
147                        toRemove = nonInvoiced;
148                        break;
149                    }
150                }
151                if (null != toRemove) {
152                    paymentApplicationDocument.getNonInvoiceds().remove(toRemove);
153                }
154            }
155    
156            // re-number the non-invoiceds
157            Integer nonInvoicedItemNumber = 1;
158            for (NonInvoiced n : paymentApplicationDocument.getNonInvoiceds()) {
159                n.setFinancialDocumentLineNumber(nonInvoicedItemNumber++);
160                n.setFinancialDocumentLineNumber(nonInvoicedItemNumber++);
161                n.refreshReferenceObject("chartOfAccounts");
162                n.refreshReferenceObject("account");
163                n.refreshReferenceObject("subAccount");
164                n.refreshReferenceObject("financialObject");
165                n.refreshReferenceObject("financialSubObject");
166                n.refreshReferenceObject("project");
167            }
168    
169            return mapping.findForward(KFSConstants.MAPPING_BASIC);
170        }
171    
172        /**
173         * Create an InvoicePaidApplied for a CustomerInvoiceDetail and validate it. If the validation succeeds the paidApplied is
174         * returned. If the validation does succeed a null is returned.
175         * 
176         * @param customerInvoiceDetail
177         * @param paymentApplicationDocument
178         * @param amount
179         * @param fieldName
180         * @return
181         * @throws WorkflowException
182         */
183        protected InvoicePaidApplied generateAndValidateNewPaidApplied(PaymentApplicationInvoiceDetailApply detailApplication, String fieldName, KualiDecimal totalFromControl) {
184    
185            // generate the paidApplied
186            InvoicePaidApplied paidApplied = detailApplication.generatePaidApplied();
187    
188            // validate the paidApplied, but ignore any failures (other than the error message)
189            PaymentApplicationDocumentRuleUtil.validateInvoicePaidApplied(paidApplied, fieldName, totalFromControl);
190    
191            // return the generated paidApplied
192            return paidApplied;
193        }
194    
195        public ActionForward applyAllAmounts(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
196            doApplicationOfFunds((PaymentApplicationDocumentForm) form);
197            return mapping.findForward(KFSConstants.MAPPING_BASIC);
198        }
199    
200        protected void doApplicationOfFunds(PaymentApplicationDocumentForm paymentApplicationDocumentForm) throws WorkflowException {
201            PaymentApplicationDocument paymentApplicationDocument = paymentApplicationDocumentForm.getPaymentApplicationDocument();
202    
203            List<InvoicePaidApplied> invoicePaidApplieds = new ArrayList<InvoicePaidApplied>();
204    
205            // apply invoice detail entries
206            invoicePaidApplieds.addAll(applyToIndividualCustomerInvoiceDetails(paymentApplicationDocumentForm));
207    
208            // quick-apply invoices
209            invoicePaidApplieds.addAll(quickApplyToInvoices(paymentApplicationDocumentForm, invoicePaidApplieds));
210    
211            // re-number the paidApplieds internal sequence numbers
212            int paidAppliedItemNumber = 1;
213            for (InvoicePaidApplied i : invoicePaidApplieds) {
214                i.setPaidAppliedItemNumber(paidAppliedItemNumber++);
215            }
216    
217            // apply non-Invoiced
218            NonInvoiced nonInvoiced = applyNonInvoiced(paymentApplicationDocumentForm);
219    
220            // apply non-applied holdings
221            NonAppliedHolding nonAppliedHolding = applyUnapplied(paymentApplicationDocumentForm);
222    
223            // sum up the paid applieds
224            KualiDecimal sumOfInvoicePaidApplieds = KualiDecimal.ZERO;
225            for (InvoicePaidApplied invoicePaidApplied : invoicePaidApplieds) {
226                KualiDecimal amount = invoicePaidApplied.getInvoiceItemAppliedAmount();
227                if (null == amount) {
228                    amount = KualiDecimal.ZERO;
229                }
230                sumOfInvoicePaidApplieds = sumOfInvoicePaidApplieds.add(amount);
231            }
232    
233            // sum up all applieds
234            KualiDecimal appliedAmount = KualiDecimal.ZERO;
235            appliedAmount = appliedAmount.add(sumOfInvoicePaidApplieds);
236            if (null != nonInvoiced && null != nonInvoiced.getFinancialDocumentLineAmount()) {
237                appliedAmount = appliedAmount.add(nonInvoiced.getFinancialDocumentLineAmount());
238            }
239            appliedAmount = appliedAmount.add(paymentApplicationDocument.getSumOfNonAppliedDistributions());
240            appliedAmount = appliedAmount.add(paymentApplicationDocument.getSumOfNonInvoicedDistributions());
241            appliedAmount = appliedAmount.add(paymentApplicationDocument.getSumOfNonInvoiceds());
242            if (null != paymentApplicationDocument.getNonAppliedHoldingAmount()) {
243                appliedAmount = appliedAmount.add(paymentApplicationDocument.getNonAppliedHoldingAmount());
244            }
245    
246            // check that we havent applied more than our control total
247            KualiDecimal controlTotalAmount = paymentApplicationDocumentForm.getTotalFromControl();
248    
249            // if the person over-applies, we dont stop them, we just complain
250            if (appliedAmount.isGreaterThan(controlTotalAmount)) {
251                addGlobalError(ArKeyConstants.PaymentApplicationDocumentErrors.CANNOT_APPLY_MORE_THAN_CASH_CONTROL_TOTAL_AMOUNT);
252            }
253    
254            // swap out the old paidApplieds with the newly generated
255            paymentApplicationDocument.getInvoicePaidApplieds().clear();
256            paymentApplicationDocument.getInvoicePaidApplieds().addAll(invoicePaidApplieds);
257    
258            // NonInvoiced list management
259            if (null != nonInvoiced) {
260                paymentApplicationDocument.getNonInvoiceds().add(nonInvoiced);
261    
262                // re-number the non-invoiced
263                Integer nonInvoicedItemNumber = 1;
264                for (NonInvoiced n : paymentApplicationDocument.getNonInvoiceds()) {
265                    n.setFinancialDocumentLineNumber(nonInvoicedItemNumber++);
266                    n.refreshReferenceObject("chartOfAccounts");
267                    n.refreshReferenceObject("account");
268                    n.refreshReferenceObject("subAccount");
269                    n.refreshReferenceObject("financialObject");
270                    n.refreshReferenceObject("financialSubObject");
271                    n.refreshReferenceObject("project");
272                }
273    
274                // make an empty new one
275                paymentApplicationDocumentForm.setNonInvoicedAddLine(new NonInvoiced());
276            }
277    
278            // reset the allocations, so it gets re-calculated
279            paymentApplicationDocumentForm.setNonAppliedControlAllocations(null);
280    
281            // Update the doc total if it is not a CashControl generated PayApp
282            if (!paymentApplicationDocument.hasCashControlDetail()) {
283                paymentApplicationDocument.getDocumentHeader().setFinancialDocumentTotalAmount(appliedAmount);
284            }
285        }
286    
287        protected List<InvoicePaidApplied> applyToIndividualCustomerInvoiceDetails(PaymentApplicationDocumentForm paymentApplicationDocumentForm) {
288            PaymentApplicationDocument paymentApplicationDocument = paymentApplicationDocumentForm.getPaymentApplicationDocument();
289            String applicationDocNbr = paymentApplicationDocument.getDocumentNumber();
290    
291            // Handle amounts applied at the invoice detail level
292            int paidAppliedsGenerated = 1;
293            int simpleInvoiceDetailApplicationCounter = 0;
294    
295            // calculate paid applieds for all invoices
296            List<InvoicePaidApplied> invoicePaidApplieds = new ArrayList<InvoicePaidApplied>();
297            for (PaymentApplicationInvoiceApply invoiceApplication : paymentApplicationDocumentForm.getInvoiceApplications()) {
298                for (PaymentApplicationInvoiceDetailApply detailApplication : invoiceApplication.getDetailApplications()) {
299    
300                    // selectedInvoiceDetailApplications[${ctr}].amountApplied
301                    String fieldName = "selectedInvoiceDetailApplications[" + Integer.toString(simpleInvoiceDetailApplicationCounter) + "].amountApplied";
302                    simpleInvoiceDetailApplicationCounter++; // needs to be incremented even if we skip this line
303    
304    
305                    // handle the user clicking full apply
306                    if (detailApplication.isFullApply()) {
307                        detailApplication.setAmountApplied(detailApplication.getAmountOpen());
308                    }
309                    // handle the user manually entering an amount
310                    else {
311                        if (detailApplication.isFullApplyChanged()) { // means it went from true to false
312                            detailApplication.setAmountApplied(KualiDecimal.ZERO);
313                        }
314                    }
315    
316                    // Don't add lines where the amount to apply is zero. Wouldn't make any sense to do that.
317                    if (KualiDecimal.ZERO.equals(detailApplication.getAmountApplied())) {
318                        continue;
319                    }
320    
321                    // generate and validate the paidApplied, and always add it to the list, even if
322                    // it fails validation. Validation failures will stop routing.
323                    GlobalVariables.getMessageMap().addToErrorPath(KFSConstants.PaymentApplicationTabErrorCodes.APPLY_TO_INVOICE_DETAIL_TAB);
324                    InvoicePaidApplied invoicePaidApplied = generateAndValidateNewPaidApplied(detailApplication, fieldName, paymentApplicationDocument.getTotalFromControl());
325                    GlobalVariables.getMessageMap().removeFromErrorPath(KFSConstants.PaymentApplicationTabErrorCodes.APPLY_TO_INVOICE_DETAIL_TAB);
326                    invoicePaidApplieds.add(invoicePaidApplied);
327                    paidAppliedsGenerated++;
328                }
329            }
330    
331            return invoicePaidApplieds;
332        }
333    
334        protected List<InvoicePaidApplied> quickApplyToInvoices(PaymentApplicationDocumentForm paymentApplicationDocumentForm, List<InvoicePaidApplied> appliedToIndividualDetails) {
335            PaymentApplicationDocument applicationDocument = (PaymentApplicationDocument) paymentApplicationDocumentForm.getDocument();
336            List<InvoicePaidApplied> invoicePaidApplieds = new ArrayList<InvoicePaidApplied>();
337    
338            // go over the selected invoices and apply full amount to each of their details
339            int index = 0;
340            for (PaymentApplicationInvoiceApply invoiceApplication : paymentApplicationDocumentForm.getInvoiceApplications()) {
341                String invoiceDocNumber = invoiceApplication.getDocumentNumber();
342    
343                // skip the line if its not set to quick apply
344                if (!invoiceApplication.isQuickApply()) {
345    
346                    // if it was just flipped from True to False
347                    if (invoiceApplication.isQuickApplyChanged()) {
348                        for (PaymentApplicationInvoiceDetailApply detailApplication : invoiceApplication.getDetailApplications()) {
349    
350                            // zero out all the details
351                            detailApplication.setAmountApplied(KualiDecimal.ZERO);
352                            detailApplication.setFullApply(false);
353    
354                            // remove any existing paidApplieds for this invoice
355                            for (int i = appliedToIndividualDetails.size() - 1; i >= 0; i--) {
356                                InvoicePaidApplied applied = appliedToIndividualDetails.get(i);
357                                if (applied.getFinancialDocumentReferenceInvoiceNumber().equals(invoiceApplication.getDocumentNumber())) {
358                                    appliedToIndividualDetails.remove(i);
359                                }
360                            }
361                        }
362                    }
363                    continue;
364                }
365    
366                // make sure none of the invoices selected have zero open amounts, complain if so
367                if (invoiceApplication.getOpenAmount().isZero()) {
368                    addGlobalError(ArKeyConstants.PaymentApplicationDocumentErrors.CANNOT_QUICK_APPLY_ON_INVOICE_WITH_ZERO_OPEN_AMOUNT);
369                    return invoicePaidApplieds;
370                }
371    
372                // remove any existing paidApplieds for this invoice
373                for (int i = appliedToIndividualDetails.size() - 1; i >= 0; i--) {
374                    InvoicePaidApplied applied = appliedToIndividualDetails.get(i);
375                    if (applied.getFinancialDocumentReferenceInvoiceNumber().equals(invoiceApplication.getDocumentNumber())) {
376                        appliedToIndividualDetails.remove(i);
377                    }
378                }
379    
380                // create and validate the paid applieds for each invoice detail
381                String fieldName = "invoiceApplications[" + invoiceDocNumber + "].quickApply";
382                for (PaymentApplicationInvoiceDetailApply detailApplication : invoiceApplication.getDetailApplications()) {
383                    detailApplication.setAmountApplied(detailApplication.getAmountOpen());
384                    detailApplication.setFullApply(true);
385                    InvoicePaidApplied paidApplied = generateAndValidateNewPaidApplied(detailApplication, fieldName, applicationDocument.getTotalFromControl());
386                    if (paidApplied != null) {
387                        invoicePaidApplieds.add(paidApplied);
388                    }
389                }
390    
391                // maintain the selected doc number
392                if (invoiceDocNumber.equals(paymentApplicationDocumentForm.getEnteredInvoiceDocumentNumber())) {
393                    paymentApplicationDocumentForm.setSelectedInvoiceDocumentNumber(invoiceDocNumber);
394                }
395            }
396    
397            return invoicePaidApplieds;
398        }
399    
400        protected NonInvoiced applyNonInvoiced(PaymentApplicationDocumentForm payAppForm) throws WorkflowException {
401            PaymentApplicationDocument applicationDocument = (PaymentApplicationDocument) payAppForm.getDocument();
402    
403            NonInvoiced nonInvoiced = payAppForm.getNonInvoicedAddLine();
404    
405            // if the line or line amount is null or zero, don't add the line. Additional validation is performed for the amount within
406            // the rules
407            // class, so no validation is needed here.
408            //
409            // NOTE: This conditional is in place because the "apply" button on the payment application document functions as a
410            // universal button,
411            // and therefore checks each tab where the button resides on the interface and attempts to apply values for that tab. This
412            // functionality
413            // causes this method to be called, regardless of if any values were entered in the "Non-AR" tab of the document. We want to
414            // ignore this
415            // method being called if there are no values entered in the fields.
416            //
417            // For the sake of this algorithm, a "Non-AR" accounting line will be ignored if it is null, or if the dollar amount entered
418            // is blank or zero.
419            if (ObjectUtils.isNull(payAppForm.getNonInvoicedAddLine()) || nonInvoiced.getFinancialDocumentLineAmount() == null || nonInvoiced.getFinancialDocumentLineAmount().isZero()) {
420                return null;
421            }
422    
423            // If we got past the above conditional, wire it up for adding
424            nonInvoiced.setFinancialDocumentPostingYear(applicationDocument.getPostingYear());
425            nonInvoiced.setDocumentNumber(applicationDocument.getDocumentNumber());
426            nonInvoiced.setFinancialDocumentLineNumber(payAppForm.getNextNonInvoicedLineNumber());
427            if (StringUtils.isNotBlank(nonInvoiced.getChartOfAccountsCode())) {
428                nonInvoiced.setChartOfAccountsCode(nonInvoiced.getChartOfAccountsCode().toUpperCase());
429            }
430    
431            // run the validations
432            boolean isValid = PaymentApplicationDocumentRuleUtil.validateNonInvoiced(nonInvoiced, applicationDocument, payAppForm.getTotalFromControl());
433    
434            // check the validation results and return null if there were any errors
435            if (!isValid) {
436                return null;
437            }
438    
439            return nonInvoiced;
440        }
441    
442        protected NonAppliedHolding applyUnapplied(PaymentApplicationDocumentForm payAppForm) throws WorkflowException {
443            PaymentApplicationDocument payAppDoc = payAppForm.getPaymentApplicationDocument();
444            String customerNumber = payAppForm.getNonAppliedHoldingCustomerNumber();
445            KualiDecimal amount = payAppForm.getNonAppliedHoldingAmount();
446    
447            // validate the customer number in the unapplied
448            if (StringUtils.isNotBlank(customerNumber)) {
449                // force customer number to upper
450                payAppForm.setNonAppliedHoldingCustomerNumber(customerNumber.toUpperCase());
451    
452                Map<String, String> pkMap = new HashMap<String, String>();
453                pkMap.put(ArPropertyConstants.CustomerFields.CUSTOMER_NUMBER, customerNumber);
454                int found = businessObjectService.countMatching(Customer.class, pkMap);
455                if (found == 0) {
456                    addFieldError(KFSConstants.PaymentApplicationTabErrorCodes.UNAPPLIED_TAB, ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_CUSTOMER_NUMBER, ArKeyConstants.PaymentApplicationDocumentErrors.ENTERED_INVOICE_CUSTOMER_NUMBER_INVALID);
457                    return null;
458                }
459            }
460    
461            // validate the amount in the unapplied
462            if (payAppForm.getNonAppliedHoldingAmount() != null && payAppForm.getNonAppliedHoldingAmount().isNegative()) {
463                addFieldError(KFSConstants.PaymentApplicationTabErrorCodes.UNAPPLIED_TAB, ArPropertyConstants.PaymentApplicationDocumentFields.UNAPPLIED_AMOUNT, ArKeyConstants.PaymentApplicationDocumentErrors.UNAPPLIED_AMOUNT_CANNOT_BE_NEGATIVE);
464                return null;
465            }
466    
467            // if we dont have enough information to make an UnApplied, then do nothing
468            if (StringUtils.isBlank(customerNumber) || amount == null || amount.isZero()) {
469                payAppDoc.setNonAppliedHolding(null);
470                return null;
471            }
472    
473            // build a new NonAppliedHolding
474            NonAppliedHolding nonAppliedHolding = new NonAppliedHolding();
475            nonAppliedHolding.setCustomerNumber(customerNumber);
476            nonAppliedHolding.setReferenceFinancialDocumentNumber(payAppDoc.getDocumentNumber());
477            nonAppliedHolding.setFinancialDocumentLineAmount(amount);
478    
479            // set it to the document
480            payAppDoc.setNonAppliedHolding(nonAppliedHolding);
481    
482            // validate it
483            boolean isValid = PaymentApplicationDocumentRuleUtil.validateNonAppliedHolding(payAppDoc, payAppForm.getTotalFromControl());
484    
485            // check the validation results and return null if there were any errors
486            if (!isValid) {
487                return null;
488            }
489    
490            return nonAppliedHolding;
491        }
492    
493        /**
494         * This method loads the invoices for currently selected customer
495         * 
496         * @param applicationDocumentForm
497         */
498        protected void loadInvoices(PaymentApplicationDocumentForm payAppForm, String selectedInvoiceNumber) {
499            PaymentApplicationDocument payAppDoc = payAppForm.getPaymentApplicationDocument();
500            AccountsReceivableDocumentHeader arDocHeader = payAppDoc.getAccountsReceivableDocumentHeader();
501            String currentInvoiceNumber = selectedInvoiceNumber;
502    
503            // before we do anything, validate the validity of any customerNumber or invoiceNumber
504            // entered against the db, and complain to the user if either is not right.
505            if (StringUtils.isNotBlank(payAppForm.getSelectedCustomerNumber())) {
506                Map<String, String> pkMap = new HashMap<String, String>();
507                pkMap.put(ArPropertyConstants.CustomerFields.CUSTOMER_NUMBER, payAppForm.getSelectedCustomerNumber());
508                int found = businessObjectService.countMatching(Customer.class, pkMap);
509                if (found == 0) {
510                    addFieldError(KFSConstants.PaymentApplicationTabErrorCodes.APPLY_TO_INVOICE_DETAIL_TAB, ArPropertyConstants.PaymentApplicationDocumentFields.ENTERED_INVOICE_CUSTOMER_NUMBER, ArKeyConstants.PaymentApplicationDocumentErrors.ENTERED_INVOICE_CUSTOMER_NUMBER_INVALID);
511                }
512            }
513            boolean validInvoice = true;
514            if (StringUtils.isNotBlank(payAppForm.getEnteredInvoiceDocumentNumber())) {
515                Map<String, String> pkMap = new HashMap<String, String>();
516                if (!SpringContext.getBean(CustomerInvoiceDocumentService.class).checkIfInvoiceNumberIsFinal(payAppForm.getEnteredInvoiceDocumentNumber())) {
517                    validInvoice &= false;
518                    addFieldError(KFSConstants.PaymentApplicationTabErrorCodes.APPLY_TO_INVOICE_DETAIL_TAB, ArPropertyConstants.PaymentApplicationDocumentFields.ENTERED_INVOICE_NUMBER, ArKeyConstants.ERROR_CUSTOMER_INVOICE_DOCUMENT_NOT_FINAL);
519                }
520            }
521    
522            // This handles the priority of the payapp selected customer number and the
523            // ar doc header customer number. The ar doc header customer number should always
524            // reflect what customer number is entered on the form for invoices. This code chunk
525            // ensures that whatever the user enters always wins, but also tries to not load the form
526            // with an empty customer number wherever possible.
527            if (StringUtils.isBlank(payAppForm.getSelectedCustomerNumber())) {
528                if (StringUtils.isBlank(arDocHeader.getCustomerNumber())) {
529                    if (payAppDoc.hasCashControlDetail()) {
530                        payAppForm.setSelectedCustomerNumber(payAppDoc.getCashControlDetail().getCustomerNumber());
531                        arDocHeader.setCustomerNumber(payAppDoc.getCashControlDetail().getCustomerNumber());
532                    }
533                }
534                else {
535                    payAppForm.setSelectedCustomerNumber(arDocHeader.getCustomerNumber());
536                }
537            }
538            else {
539                arDocHeader.setCustomerNumber(payAppForm.getSelectedCustomerNumber());
540            }
541            String customerNumber = payAppForm.getSelectedCustomerNumber();
542    
543            // Invoice number entered, but no customer number entered
544            if (StringUtils.isBlank(customerNumber) && StringUtils.isNotBlank(currentInvoiceNumber) && validInvoice) {
545                Customer customer = customerInvoiceDocumentService.getCustomerByInvoiceDocumentNumber(currentInvoiceNumber);
546                customerNumber = customer.getCustomerNumber();
547                payAppDoc.getAccountsReceivableDocumentHeader().setCustomerNumber(customerNumber);
548            }
549    
550            // load up the control docs and non-applied holdings for non-cash-control payapps
551            if (StringUtils.isNotBlank(customerNumber)) {
552                if (!payAppDoc.hasCashControlDocument()) {
553                    List<PaymentApplicationDocument> nonAppliedControlDocs = new ArrayList<PaymentApplicationDocument>();
554                    List<NonAppliedHolding> nonAppliedControlHoldings = new ArrayList<NonAppliedHolding>();
555    
556                    // if the doc is already final/approved, then we only pull the relevant control
557                    // documents and nonapplied holdings that this doc paid against.
558                    if (payAppDoc.isFinal()) {
559                        nonAppliedControlDocs.addAll(payAppDoc.getPaymentApplicationDocumentsUsedAsControlDocuments());
560                        nonAppliedControlHoldings.addAll(payAppDoc.getNonAppliedHoldingsUsedAsControls());
561                    }
562    
563                    // otherwise, we pull all available non-zero non-applied holdings for
564                    // this customer, and make the associated docs and non-applied holdings available
565                    else {
566                        // retrieve the set of available non-applied holdings for this customer
567                        NonAppliedHoldingService nonAppliedHoldingService = SpringContext.getBean(NonAppliedHoldingService.class);
568                        nonAppliedControlHoldings.addAll(nonAppliedHoldingService.getNonAppliedHoldingsForCustomer(customerNumber));
569    
570                        // get the parent list of payapp documents that they come from
571                        List<String> controlDocNumbers = new ArrayList<String>();
572                        for (NonAppliedHolding nonAppliedHolding : nonAppliedControlHoldings) {
573                            if (nonAppliedHolding.getAvailableUnappliedAmount().isPositive()) {
574                                if (!controlDocNumbers.contains(nonAppliedHolding.getReferenceFinancialDocumentNumber())) {
575                                    controlDocNumbers.add(nonAppliedHolding.getReferenceFinancialDocumentNumber());
576                                }
577                            }
578                        }
579                        // only try to retrieve docs if we have any to retrieve
580                        if (!controlDocNumbers.isEmpty()) {
581                            try {
582                                nonAppliedControlDocs.addAll(documentService.getDocumentsByListOfDocumentHeaderIds(PaymentApplicationDocument.class, controlDocNumbers));
583                            }
584                            catch (WorkflowException e) {
585                                throw new RuntimeException("A runtimeException was thrown when trying to retrieve a list of documents.", e);
586                            }
587                        }
588                    }
589    
590                    // set the form vars from what we've loaded up here
591                    payAppForm.setNonAppliedControlDocs(nonAppliedControlDocs);
592                    payAppForm.setNonAppliedControlHoldings(nonAppliedControlHoldings);
593                    payAppDoc.setNonAppliedHoldingsForCustomer(new ArrayList<NonAppliedHolding>(nonAppliedControlHoldings));
594                    payAppForm.setNonAppliedControlAllocations(null);
595                }
596            }
597    
598            // reload invoices for the selected customer number
599            if (StringUtils.isNotBlank(customerNumber)) {
600                Collection<CustomerInvoiceDocument> openInvoicesForCustomer;
601    
602                // we have to special case the invoices once the document is finished, because
603                // at this point, we want to show the invoices it paid against, NOT the set of
604                // open invoices
605                if (payAppDoc.isFinal()) {
606                    openInvoicesForCustomer = payAppDoc.getInvoicesPaidAgainst();
607                }
608                else {
609                    openInvoicesForCustomer = customerInvoiceDocumentService.getOpenInvoiceDocumentsByCustomerNumber(customerNumber);
610                }
611                payAppForm.setInvoices(new ArrayList<CustomerInvoiceDocument>(openInvoicesForCustomer));
612                payAppForm.setupInvoiceWrappers(payAppDoc.getDocumentNumber());
613            }
614    
615            // if no invoice number entered than get the first invoice
616            if (StringUtils.isNotBlank(customerNumber) && StringUtils.isBlank(currentInvoiceNumber)) {
617                if (payAppForm.getInvoices() == null || payAppForm.getInvoices().isEmpty()) {
618                    currentInvoiceNumber = null;
619                }
620                else {
621                    currentInvoiceNumber = payAppForm.getInvoices().get(0).getDocumentNumber();
622                }
623            }
624    
625            // load information for the current selected invoice
626            if (StringUtils.isNotBlank(currentInvoiceNumber)) {
627                payAppForm.setSelectedInvoiceDocumentNumber(currentInvoiceNumber);
628                payAppForm.setEnteredInvoiceDocumentNumber(currentInvoiceNumber);
629            }
630    
631            // make sure all paidApplieds are synched with the PaymentApplicationInvoiceApply and
632            // PaymentApplicationInvoiceDetailApply objects, so that the form reflects how it was left pre-save.
633            // This is only necessary when the doc is saved, and then re-opened, as the invoice-detail wrappers
634            // will no longer hold the state info. I know this is a monstrosity. Get over it.
635            for (InvoicePaidApplied paidApplied : payAppDoc.getInvoicePaidApplieds()) {
636                for (PaymentApplicationInvoiceApply invoiceApplication : payAppForm.getInvoiceApplications()) {
637                    if (paidApplied.getFinancialDocumentReferenceInvoiceNumber().equalsIgnoreCase(invoiceApplication.getDocumentNumber())) {
638                        for (PaymentApplicationInvoiceDetailApply detailApplication : invoiceApplication.getDetailApplications()) {
639                            if (paidApplied.getInvoiceItemNumber().equals(detailApplication.getSequenceNumber())) {
640    
641                                // if the amount applieds dont match, then have the paidApplied fill in the applied amounts
642                                // for the invoiceApplication details
643                                if (!paidApplied.getInvoiceItemAppliedAmount().equals(detailApplication.getAmountApplied())) {
644                                    detailApplication.setAmountApplied(paidApplied.getInvoiceItemAppliedAmount());
645                                    if (paidApplied.getInvoiceItemAppliedAmount().equals(detailApplication.getAmountOpen())) {
646                                        detailApplication.setFullApply(true);
647                                    }
648                                }
649                            }
650                        }
651                    }
652                }
653            }
654    
655            // clear any NonInvoiced add line information from the form vars
656            payAppForm.setNonInvoicedAddLine(null);
657    
658            // load any NonAppliedHolding information into the form vars
659            if (payAppDoc.getNonAppliedHolding() != null) {
660                payAppForm.setNonAppliedHoldingCustomerNumber(payAppDoc.getNonAppliedHolding().getCustomerNumber());
661                payAppForm.setNonAppliedHoldingAmount(payAppDoc.getNonAppliedHolding().getFinancialDocumentLineAmount());
662            }
663            else {
664                // clear any NonAppliedHolding information from the form vars if it's empty
665                payAppForm.setNonAppliedHoldingCustomerNumber(null);
666                payAppForm.setNonAppliedHoldingAmount(null);
667            }
668        }
669    
670        /**
671         * This method updates the customer invoice details when a new invoice is selected
672         * 
673         * @param mapping
674         * @param form
675         * @param request
676         * @param response
677         * @return
678         * @throws Exception
679         */
680        public ActionForward goToInvoice(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
681            PaymentApplicationDocumentForm payAppForm = (PaymentApplicationDocumentForm) form;
682            loadInvoices(payAppForm, payAppForm.getSelectedInvoiceDocumentNumber());
683            if (!payAppForm.getPaymentApplicationDocument().isFinal()) {
684                doApplicationOfFunds(payAppForm);
685            }
686            return mapping.findForward(KFSConstants.MAPPING_BASIC);
687        }
688    
689        /**
690         * This method updates customer invoice details when next invoice is selected
691         * 
692         * @param mapping
693         * @param form
694         * @param request
695         * @param response
696         * @return
697         * @throws Exception
698         */
699        public ActionForward goToNextInvoice(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
700            PaymentApplicationDocumentForm payAppForm = (PaymentApplicationDocumentForm) form;
701            loadInvoices(payAppForm, payAppForm.getNextInvoiceDocumentNumber());
702            if (!payAppForm.getPaymentApplicationDocument().isFinal()) {
703                doApplicationOfFunds(payAppForm);
704            }
705            return mapping.findForward(KFSConstants.MAPPING_BASIC);
706        }
707    
708        /**
709         * This method updates customer invoice details when previous invoice is selected
710         * 
711         * @param mapping
712         * @param form
713         * @param request
714         * @param response
715         * @return
716         * @throws Exception
717         */
718        public ActionForward goToPreviousInvoice(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
719            PaymentApplicationDocumentForm payAppForm = (PaymentApplicationDocumentForm) form;
720            loadInvoices(payAppForm, payAppForm.getPreviousInvoiceDocumentNumber());
721            if (!payAppForm.getPaymentApplicationDocument().isFinal()) {
722                doApplicationOfFunds(payAppForm);
723            }
724            return mapping.findForward(KFSConstants.MAPPING_BASIC);
725        }
726    
727        /**
728         * Retrieve all invoices for the selected customer.
729         * 
730         * @param mapping
731         * @param form
732         * @param request
733         * @param response
734         * @return
735         * @throws Exception
736         */
737        public ActionForward loadInvoices(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
738            PaymentApplicationDocumentForm pform = (PaymentApplicationDocumentForm) form;
739            loadInvoices(pform, pform.getEnteredInvoiceDocumentNumber());
740            return mapping.findForward(KFSConstants.MAPPING_BASIC);
741        }
742    
743        /**
744         * Cancel the document.
745         * 
746         * @see org.kuali.rice.kns.web.struts.action.KualiDocumentActionBase#cancel(org.apache.struts.action.ActionMapping,
747         *      org.apache.struts.action.ActionForm, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
748         */
749        @Override
750        public ActionForward cancel(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
751            PaymentApplicationDocumentForm _form = (PaymentApplicationDocumentForm) form;
752            if (null == _form.getCashControlDocument()) {
753                return super.cancel(mapping, form, request, response);
754            }
755            return mapping.findForward(KFSConstants.MAPPING_BASIC);
756        }
757    
758        /**
759         * @see org.kuali.rice.kns.web.struts.action.KualiDocumentActionBase#createDocument(org.kuali.rice.kns.web.struts.form.KualiDocumentFormBase)
760         */
761        @Override
762        protected void createDocument(KualiDocumentFormBase kualiDocumentFormBase) throws WorkflowException {
763            super.createDocument(kualiDocumentFormBase);
764            PaymentApplicationDocumentForm form = (PaymentApplicationDocumentForm) kualiDocumentFormBase;
765            PaymentApplicationDocument document = form.getPaymentApplicationDocument();
766    
767            // create new accounts receivable header and set it to the payment application document
768            AccountsReceivableDocumentHeaderService accountsReceivableDocumentHeaderService = SpringContext.getBean(AccountsReceivableDocumentHeaderService.class);
769            AccountsReceivableDocumentHeader accountsReceivableDocumentHeader = accountsReceivableDocumentHeaderService.getNewAccountsReceivableDocumentHeaderForCurrentUser();
770            accountsReceivableDocumentHeader.setDocumentNumber(document.getDocumentNumber());
771            document.setAccountsReceivableDocumentHeader(accountsReceivableDocumentHeader);
772        }
773    
774        /**
775         * @see org.kuali.rice.kns.web.struts.action.KualiDocumentActionBase#loadDocument(org.kuali.rice.kns.web.struts.form.KualiDocumentFormBase)
776         */
777        @Override
778        protected void loadDocument(KualiDocumentFormBase kualiDocumentFormBase) throws WorkflowException {
779            super.loadDocument(kualiDocumentFormBase);
780            PaymentApplicationDocumentForm pform = (PaymentApplicationDocumentForm) kualiDocumentFormBase;
781            loadInvoices(pform, pform.getEnteredInvoiceDocumentNumber());
782        }
783    
784        /**
785         * Get an error to display in the UI for a certain field.
786         * 
787         * @param propertyName
788         * @param errorKey
789         */
790        protected void addFieldError(String errorPathToAdd, String propertyName, String errorKey) {
791            GlobalVariables.getMessageMap().addToErrorPath(errorPathToAdd);
792            GlobalVariables.getMessageMap().putError(propertyName, errorKey);
793            GlobalVariables.getMessageMap().removeFromErrorPath(errorPathToAdd);
794        }
795    
796        /**
797         * Get an error to display at the global level, for the whole document.
798         * 
799         * @param errorKey
800         */
801        protected void addGlobalError(String errorKey) {
802            GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KNSConstants.DOCUMENT_ERRORS, errorKey, "document.hiddenFieldForErrors");
803        }
804    
805    }