View Javadoc

1   /*
2    * Copyright 2007 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.ole.module.purap.document.service.impl;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.apache.commons.lang.text.StrBuilder;
21  import org.kuali.ole.module.purap.*;
22  import org.kuali.ole.module.purap.PurapConstants.ItemTypeCodes;
23  import org.kuali.ole.module.purap.PurapConstants.PREQDocumentsStrings;
24  import org.kuali.ole.module.purap.PurapConstants.PaymentRequestStatuses;
25  import org.kuali.ole.module.purap.PurapParameterConstants.NRATaxParameters;
26  import org.kuali.ole.module.purap.businessobject.*;
27  import org.kuali.ole.module.purap.document.AccountsPayableDocument;
28  import org.kuali.ole.module.purap.document.PaymentRequestDocument;
29  import org.kuali.ole.module.purap.document.PurchaseOrderDocument;
30  import org.kuali.ole.module.purap.document.VendorCreditMemoDocument;
31  import org.kuali.ole.module.purap.document.dataaccess.PaymentRequestDao;
32  import org.kuali.ole.module.purap.document.service.*;
33  import org.kuali.ole.module.purap.document.validation.event.AttributedContinuePurapEvent;
34  import org.kuali.ole.module.purap.exception.PurError;
35  import org.kuali.ole.module.purap.service.PurapAccountingService;
36  import org.kuali.ole.module.purap.service.PurapGeneralLedgerService;
37  import org.kuali.ole.module.purap.util.ExpiredOrClosedAccountEntry;
38  import org.kuali.ole.module.purap.util.PurApItemUtils;
39  import org.kuali.ole.module.purap.util.VendorGroupingHelper;
40  import org.kuali.ole.sys.OLEPropertyConstants;
41  import org.kuali.ole.sys.businessobject.AccountingLine;
42  import org.kuali.ole.sys.businessobject.Bank;
43  import org.kuali.ole.sys.businessobject.SourceAccountingLine;
44  import org.kuali.ole.sys.context.SpringContext;
45  import org.kuali.ole.sys.service.BankService;
46  import org.kuali.ole.sys.service.FinancialSystemWorkflowHelperService;
47  import org.kuali.ole.sys.service.UniversityDateService;
48  import org.kuali.ole.sys.service.impl.OleParameterConstants;
49  import org.kuali.ole.vnd.VendorConstants;
50  import org.kuali.ole.vnd.businessobject.PaymentTermType;
51  import org.kuali.ole.vnd.businessobject.VendorAddress;
52  import org.kuali.ole.vnd.businessobject.VendorDetail;
53  import org.kuali.ole.vnd.document.service.VendorService;
54  import org.kuali.rice.core.api.config.property.ConfigurationService;
55  import org.kuali.rice.core.api.datetime.DateTimeService;
56  import org.kuali.rice.core.api.util.type.KualiDecimal;
57  import org.kuali.rice.coreservice.framework.parameter.ParameterService;
58  import org.kuali.rice.kew.api.KewApiServiceLocator;
59  import org.kuali.rice.kew.api.WorkflowDocument;
60  import org.kuali.rice.kew.api.document.search.DocumentSearchCriteria;
61  import org.kuali.rice.kew.api.document.search.DocumentSearchResult;
62  import org.kuali.rice.kew.api.document.search.DocumentSearchResults;
63  import org.kuali.rice.kew.api.exception.WorkflowException;
64  import org.kuali.rice.kim.api.identity.Person;
65  import org.kuali.rice.kns.service.DataDictionaryService;
66  import org.kuali.rice.krad.bo.DocumentHeader;
67  import org.kuali.rice.krad.bo.Note;
68  import org.kuali.rice.krad.exception.ValidationException;
69  import org.kuali.rice.krad.service.BusinessObjectService;
70  import org.kuali.rice.krad.service.DocumentService;
71  import org.kuali.rice.krad.service.KualiRuleService;
72  import org.kuali.rice.krad.service.NoteService;
73  import org.kuali.rice.krad.util.GlobalVariables;
74  import org.kuali.rice.krad.util.KRADPropertyConstants;
75  import org.kuali.rice.krad.util.ObjectUtils;
76  import org.kuali.rice.krad.workflow.service.WorkflowDocumentService;
77  import org.springframework.transaction.annotation.Transactional;
78  
79  import java.math.BigDecimal;
80  import java.sql.Date;
81  import java.sql.Timestamp;
82  import java.util.*;
83  
84  /**
85   * This class provides services of use to a payment request document
86   */
87  @Transactional
88  public class PaymentRequestServiceImpl implements PaymentRequestService {
89      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PaymentRequestServiceImpl.class);
90  
91      protected DateTimeService dateTimeService;
92      protected DocumentService documentService;
93      protected NoteService noteService;
94      protected PurapService purapService;
95      protected PaymentRequestDao paymentRequestDao;
96      protected ParameterService parameterService;
97      protected ConfigurationService configurationService;
98      protected NegativePaymentRequestApprovalLimitService negativePaymentRequestApprovalLimitService;
99      protected PurapAccountingService purapAccountingService;
100     protected BusinessObjectService businessObjectService;
101     protected PurApWorkflowIntegrationService purapWorkflowIntegrationService;
102     protected WorkflowDocumentService workflowDocumentService;
103     protected AccountsPayableService accountsPayableService;
104     protected VendorService vendorService;
105     protected DataDictionaryService dataDictionaryService;
106     protected UniversityDateService universityDateService;
107     protected BankService bankService;
108     protected PurchaseOrderService purchaseOrderService;
109     protected FinancialSystemWorkflowHelperService financialSystemWorkflowHelperService;
110     protected KualiRuleService kualiRuleService;
111 
112     /**
113      * NOTE: unused
114      *
115      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#getPaymentRequestsToExtractByCM(java.lang.String, org.kuali.ole.module.purap.document.VendorCreditMemoDocument)
116      */
117     @Override
118     @Deprecated
119     public Collection<PaymentRequestDocument> getPaymentRequestsToExtractByCM(String campusCode, VendorCreditMemoDocument cmd) {
120         LOG.debug("getPaymentRequestsByCM() started");
121         Date currentSqlDateMidnight = dateTimeService.getCurrentSqlDateMidnight();
122         List<PaymentRequestDocument> paymentRequestIterator = paymentRequestDao.getPaymentRequestsToExtract(campusCode, null, null, cmd.getVendorHeaderGeneratedIdentifier(), cmd.getVendorDetailAssignedIdentifier(), currentSqlDateMidnight);
123 
124         return filterPaymentRequestByAppDocStatus(paymentRequestIterator,
125                 PurapConstants.PaymentRequestStatuses.APPDOC_AUTO_APPROVED,
126                 PurapConstants.PaymentRequestStatuses.APPDOC_DEPARTMENT_APPROVED);
127     }
128 
129 
130     /**
131      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#getPaymentRequestsToExtractByVendor(java.lang.String,
132      *      org.kuali.ole.module.purap.util.VendorGroupingHelper, java.sql.Date)
133      */
134     @Override
135     public Collection<PaymentRequestDocument> getPaymentRequestsToExtractByVendor(String campusCode, VendorGroupingHelper vendor, Date onOrBeforePaymentRequestPayDate) {
136         LOG.debug("getPaymentRequestsByVendor() started");
137         Collection<PaymentRequestDocument> paymentRequestDocuments = paymentRequestDao.getPaymentRequestsToExtractForVendor(campusCode, vendor, onOrBeforePaymentRequestPayDate);
138 
139         return filterPaymentRequestByAppDocStatus(paymentRequestDocuments,
140                 PurapConstants.PaymentRequestStatuses.APPDOC_AUTO_APPROVED,
141                 PurapConstants.PaymentRequestStatuses.APPDOC_DEPARTMENT_APPROVED);
142     }
143 
144     /**
145      * @see org.kuali.module.purap.server.PaymentRequestService.getPaymentRequestsToExtract(Date)
146      */
147     @Override
148     public Collection<PaymentRequestDocument> getPaymentRequestsToExtract(Date onOrBeforePaymentRequestPayDate) {
149         LOG.debug("getPaymentRequestsToExtract() started");
150 
151         Collection<PaymentRequestDocument> paymentRequestIterator = paymentRequestDao.getPaymentRequestsToExtract(false, null, onOrBeforePaymentRequestPayDate);
152         return filterPaymentRequestByAppDocStatus(paymentRequestIterator,
153                 PaymentRequestStatuses.STATUSES_ALLOWED_FOR_EXTRACTION);
154     }
155 
156     /**
157      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#getPaymentRequestsToExtractSpecialPayments(java.lang.String,
158      *      java.sql.Date)
159      */
160     @Override
161     public Collection<PaymentRequestDocument> getPaymentRequestsToExtractSpecialPayments(String chartCode, Date onOrBeforePaymentRequestPayDate) {
162         LOG.debug("getPaymentRequestsToExtractSpecialPayments() started");
163 
164         Collection<PaymentRequestDocument> paymentRequestIterator = paymentRequestDao.getPaymentRequestsToExtract(true, chartCode, onOrBeforePaymentRequestPayDate);
165         return filterPaymentRequestByAppDocStatus(paymentRequestIterator,
166                 PaymentRequestStatuses.STATUSES_ALLOWED_FOR_EXTRACTION);
167     }
168 
169     /**
170      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#getImmediatePaymentRequestsToExtract(java.lang.String)
171      */
172     @Override
173     public Collection<PaymentRequestDocument> getImmediatePaymentRequestsToExtract(String chartCode) {
174         LOG.debug("getImmediatePaymentRequestsToExtract() started");
175 
176         Collection<PaymentRequestDocument> paymentRequestIterator = paymentRequestDao.getImmediatePaymentRequestsToExtract(chartCode);
177         return filterPaymentRequestByAppDocStatus(paymentRequestIterator,
178                 PaymentRequestStatuses.STATUSES_ALLOWED_FOR_EXTRACTION);
179     }
180 
181     /**
182      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#getPaymentRequestToExtractByChart(java.lang.String,
183      *      java.sql.Date)
184      */
185     @Override
186     public Collection<PaymentRequestDocument> getPaymentRequestToExtractByChart(String chartCode, Date onOrBeforePaymentRequestPayDate) {
187         LOG.debug("getPaymentRequestToExtractByChart() started");
188 
189         Collection<PaymentRequestDocument> paymentRequestIterator = paymentRequestDao.getPaymentRequestsToExtract(false, chartCode, onOrBeforePaymentRequestPayDate);
190         return filterPaymentRequestByAppDocStatus(paymentRequestIterator,
191                 PaymentRequestStatuses.STATUSES_ALLOWED_FOR_EXTRACTION);
192     }
193 
194     /**
195      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService.autoApprovePaymentRequests()
196      */
197     @Override
198     public boolean autoApprovePaymentRequests() {
199         if (LOG.isInfoEnabled()) {
200             LOG.info("Starting autoApprovePaymentRequests.");
201         }
202         boolean hadErrorAtLeastOneError = true;
203         // should objects from existing user session be copied over
204         Date todayAtMidnight = dateTimeService.getCurrentSqlDateMidnight();
205 
206         List<String> docNumbers = paymentRequestDao.getEligibleForAutoApproval(todayAtMidnight);
207         docNumbers = filterPaymentRequestByAppDocStatus(docNumbers, PurapConstants.PaymentRequestStatuses.PREQ_STATUSES_FOR_AUTO_APPROVE);
208         if (LOG.isInfoEnabled()) {
209             LOG.info(" -- Initial filtering complete, returned " + new Integer(docNumbers.size()).toString() + " docs.");
210         }
211 
212         List<PaymentRequestDocument> docs = new ArrayList<PaymentRequestDocument>();
213         for (String docNumber : docNumbers) {
214             PaymentRequestDocument preq = getPaymentRequestByDocumentNumber(docNumber);
215             if (ObjectUtils.isNotNull(preq)) {
216                 docs.add(preq);
217             }
218         }
219         if (LOG.isInfoEnabled()) {
220             LOG.info(" -- Initial filtering complete, returned " + new Integer((docs == null ? 0 : docs.size())).toString() + " docs.");
221         }
222         if (docs != null) {
223             String samt = parameterService.getParameterValueAsString(PaymentRequestDocument.class, PurapParameterConstants.PURAP_DEFAULT_NEGATIVE_PAYMENT_REQUEST_APPROVAL_LIMIT);
224             KualiDecimal defaultMinimumLimit = new KualiDecimal(samt);
225             if (LOG.isInfoEnabled()) {
226                 LOG.info(" -- Using default limit value of " + defaultMinimumLimit.toString() + ".");
227             }
228             for (PaymentRequestDocument paymentRequestDocument : docs) {
229                 hadErrorAtLeastOneError |= !autoApprovePaymentRequest(paymentRequestDocument, defaultMinimumLimit);
230             }
231         }
232         return hadErrorAtLeastOneError;
233     }
234 
235     /**
236      * NOTE: in the event of auto-approval failure, this method may throw a RuntimeException, indicating to Spring transactional
237      * management that the transaction should be rolled back.
238      *
239      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#autoApprovePaymentRequest(java.lang.String,
240      *      org.kuali.rice.core.api.util.type.KualiDecimal)
241      */
242     @Override
243     public boolean autoApprovePaymentRequest(String docNumber, KualiDecimal defaultMinimumLimit) {
244         PaymentRequestDocument paymentRequestDocument = null;
245         try {
246             paymentRequestDocument = (PaymentRequestDocument) documentService.getByDocumentHeaderId(docNumber);
247             if (paymentRequestDocument.isHoldIndicator() || paymentRequestDocument.isPaymentRequestedCancelIndicator() || !Arrays.asList(PurapConstants.PaymentRequestStatuses.PREQ_STATUSES_FOR_AUTO_APPROVE).contains(paymentRequestDocument.getApplicationDocumentStatus())) {
248                 // this condition is based on the conditions that PaymentRequestDaoOjb.getEligibleDocumentNumbersForAutoApproval()
249                 // uses to query
250                 // the database. Rechecking these conditions to ensure that the document is eligible for auto-approval, because
251                 // we're not running things
252                 // within the same transaction anymore and changes could have occurred since we called that method that make this
253                 // document not auto-approvable
254 
255                 // note that this block does not catch all race conditions
256                 // however, this error condition is not enough to make us return an error code, so just skip the document
257                 LOG.warn("Payment Request Document " + paymentRequestDocument.getDocumentNumber() + " could not be auto-approved because it has either been placed on hold, " + " requested cancel, or does not have one of the PREQ statuses for auto-approve.");
258                 return true;
259             }
260             if (autoApprovePaymentRequest(paymentRequestDocument, defaultMinimumLimit)) {
261                 if (LOG.isInfoEnabled()) {
262                     LOG.info("Auto-approval for payment request successful.  Doc number: " + docNumber);
263                 }
264                 return true;
265             } else {
266                 LOG.error("Payment Request Document " + docNumber + " could not be auto-approved.");
267                 return false;
268             }
269         } catch (WorkflowException we) {
270             LOG.error("Exception encountered when retrieving document number " + docNumber + ".", we);
271             // throw a runtime exception up so that we can force a rollback
272             throw new RuntimeException("Exception encountered when retrieving document number " + docNumber + ".", we);
273         }
274     }
275 
276     /**
277      * NOTE: in the event of auto-approval failure, this method may throw a RuntimeException, indicating to Spring transactional
278      * management that the transaction should be rolled back.
279      *
280      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#autoApprovePaymentRequest(org.kuali.ole.module.purap.document.PaymentRequestDocument,
281      *      org.kuali.rice.core.api.util.type.KualiDecimal)
282      */
283     @Override
284     public boolean autoApprovePaymentRequest(PaymentRequestDocument doc, KualiDecimal defaultMinimumLimit) {
285         if (isEligibleForAutoApproval(doc, defaultMinimumLimit)) {
286             try {
287                 // Much of the rice frameworks assumes that document instances that are saved via DocumentService.saveDocument are
288                 // those
289                 // that were dynamically created by PojoFormBase (i.e., the Document instance wasn't created from OJB). We need to
290                 // make
291                 // a deep copy and materialize collections to fulfill that assumption so that collection elements will delete
292                 // properly
293 
294                 // TODO: maybe rewriting PurapService.calculateItemTax could be rewritten so that the a deep copy doesn't need to be
295                 // made
296                 // by taking advantage of OJB's managed array lists
297                 try {
298                     ObjectUtils.materializeUpdateableCollections(doc);
299                     for (PaymentRequestItem item : (List<PaymentRequestItem>) doc.getItems()) {
300                         ObjectUtils.materializeUpdateableCollections(item);
301                     }
302                 } catch (Exception ex) {
303                     throw new RuntimeException(ex);
304                 }
305                 doc = (PaymentRequestDocument) ObjectUtils.deepCopy(doc);
306                 //purapService.updateStatus(doc, PaymentRequestStatuses.AUTO_APPROVED);
307                 doc.updateAndSaveAppDocStatus(PaymentRequestStatuses.APPDOC_AUTO_APPROVED);
308 
309                 documentService.blanketApproveDocument(doc, "auto-approving: Total is below threshold.", null);
310             } catch (WorkflowException we) {
311                 LOG.error("Exception encountered when approving document number " + doc.getDocumentNumber() + ".", we);
312                 // throw a runtime exception up so that we can force a rollback
313                 throw new RuntimeException("Exception encountered when approving document number " + doc.getDocumentNumber() + ".", we);
314             }
315         }
316         return true;
317     }
318 
319     public boolean autoApprovePaymentRequest(PaymentRequestDocument doc) {
320         try {
321             // Much of the rice frameworks assumes that document instances that are saved via DocumentService.saveDocument are
322             // those
323             // that were dynamically created by PojoFormBase (i.e., the Document instance wasn't created from OJB). We need to
324             // make
325             // a deep copy and materialize collections to fulfill that assumption so that collection elements will delete
326             // properly
327 
328             // TODO: maybe rewriting PurapService.calculateItemTax could be rewritten so that the a deep copy doesn't need to be
329             // made
330             // by taking advantage of OJB's managed array lists
331             try {
332                 ObjectUtils.materializeUpdateableCollections(doc);
333                 for (PaymentRequestItem item : (List<PaymentRequestItem>) doc.getItems()) {
334                     ObjectUtils.materializeUpdateableCollections(item);
335                 }
336             } catch (Exception ex) {
337                 throw new RuntimeException(ex);
338             }
339             doc = (PaymentRequestDocument) ObjectUtils.deepCopy(doc);
340             //purapService.updateStatus(doc, PaymentRequestStatuses.AUTO_APPROVED);
341             doc.updateAndSaveAppDocStatus(PaymentRequestStatuses.APPDOC_AUTO_APPROVED);
342 
343             documentService.blanketApproveDocument(doc, "auto-approving: Total is below threshold.", null);
344         } catch (WorkflowException we) {
345             LOG.error("Exception encountered when approving document number " + doc.getDocumentNumber() + ".", we);
346             // throw a runtime exception up so that we can force a rollback
347             throw new RuntimeException("Exception encountered when approving document number " + doc.getDocumentNumber() + ".", we);
348         }
349         return true;
350     }
351 
352     /**
353      * Determines whether or not a payment request document can be automatically approved. FYI - If fiscal reviewers are allowed to
354      * save account changes without the full account validation running then this method must call full account validation to make
355      * sure auto approver is not blanket approving an invalid document according the the accounts on the items
356      *
357      * @param document            The payment request document to be determined whether it can be automatically approved.
358      * @param defaultMinimumLimit The amount to be used as the minimum amount if no limit was found or the default is less than the
359      *                            limit.
360      * @return boolean true if the payment request document is eligible for auto approval.
361      */
362     protected boolean isEligibleForAutoApproval(PaymentRequestDocument document, KualiDecimal defaultMinimumLimit) {
363         // Check if vendor is foreign.
364         if (document.getVendorDetail().getVendorHeader().getVendorForeignIndicator().booleanValue()) {
365             if (LOG.isInfoEnabled()) {
366                 LOG.info(" -- PayReq [" + document.getDocumentNumber() + "] skipped due to a Foreign Vendor.");
367             }
368             return false;
369         }
370 
371         // check to make sure the payment request isn't scheduled to stop in tax review.
372         if (purapWorkflowIntegrationService.willDocumentStopAtGivenFutureRouteNode(document, PaymentRequestStatuses.NODE_VENDOR_TAX_REVIEW)) {
373             if (LOG.isInfoEnabled()) {
374                 LOG.info(" -- PayReq [" + document.getDocumentNumber() + "] skipped due to requiring Tax Review.");
375             }
376             return false;
377         }
378 
379         // Change to not auto approve if positive approval required indicator set to Yes
380         if (document.isPaymentRequestPositiveApprovalIndicator()) {
381             if (LOG.isInfoEnabled()) {
382                 LOG.info(" -- PayReq [" + document.getDocumentNumber()
383                         + "] skipped due to a Positive Approval Required Indicator set to Yes.");
384             }
385             return false;
386         }
387 
388         // This minimum will be set to the minimum limit derived from all
389         // accounting lines on the document. If no limit is determined, the
390         // default will be used.
391         KualiDecimal minimumAmount = null;
392 
393         // Iterate all source accounting lines on the document, deriving a
394         // minimum limit from each according to chart, chart and account, and
395         // chart and organization.
396         for (SourceAccountingLine line : purapAccountingService.generateSummary(document.getItems())) {
397             // check to make sure the account is in the auto approve exclusion list
398             Map<String, Object> autoApproveMap = new HashMap<String, Object>();
399             autoApproveMap.put("chartOfAccountsCode", line.getChartOfAccountsCode());
400             autoApproveMap.put("accountNumber", line.getAccountNumber());
401             autoApproveMap.put("active", true);
402             AutoApproveExclude autoApproveExclude = businessObjectService.findByPrimaryKey(AutoApproveExclude.class, autoApproveMap);
403             if (autoApproveExclude != null) {
404                 if (LOG.isInfoEnabled()) {
405                     LOG.info(" -- PayReq [" + document.getDocumentNumber() + "] skipped due to source accounting line "
406                             + line.getSequenceNumber() + " using Chart/Account [" + line.getChartOfAccountsCode() + "-"
407                             + line.getAccountNumber() + "], which is excluded in the Auto Approve Exclusions table.");
408                 }
409                 return false;
410             }
411 
412             minimumAmount = getMinimumLimitAmount(negativePaymentRequestApprovalLimitService.findByChart(line.getChartOfAccountsCode()), minimumAmount);
413             minimumAmount = getMinimumLimitAmount(negativePaymentRequestApprovalLimitService.findByChartAndAccount(line.getChartOfAccountsCode(), line.getAccountNumber()), minimumAmount);
414             minimumAmount = getMinimumLimitAmount(negativePaymentRequestApprovalLimitService.findByChartAndOrganization(line.getChartOfAccountsCode(), line.getOrganizationReferenceId()), minimumAmount);
415         }
416 
417         // If Receiving required is set, it's not needed to check the negative payment request approval limit
418         if (document.isReceivingDocumentRequiredIndicator()) {
419             if (LOG.isInfoEnabled()) {
420                 LOG.info(" -- PayReq ["
421                         + document.getDocumentNumber()
422                         + "] auto-approved (ignored dollar limit) due to Receiving Document Required Indicator set to Yes.");
423             }
424             return true;
425         }
426 
427         // If no limit was found or the default is less than the limit, the default limit is used.
428         if (ObjectUtils.isNull(minimumAmount) || defaultMinimumLimit.compareTo(minimumAmount) < 0) {
429             minimumAmount = defaultMinimumLimit;
430         }
431 
432         // The document is eligible for auto-approval if the document total is below the limit.
433         if (document.getFinancialSystemDocumentHeader().getFinancialDocumentTotalAmount().isLessThan(minimumAmount)) {
434             if (LOG.isInfoEnabled()) {
435                 LOG.info(" -- PayReq ["
436                         + document.getDocumentNumber()
437                         + "] auto-approved due to document Total ["
438                         + document.getFinancialSystemDocumentHeader().getFinancialDocumentTotalAmount()
439                         + "] being less than "
440                         + (minimumAmount == defaultMinimumLimit ? "Default Auto-Approval Limit "
441                         : "Configured Auto-Approval Limit ") + "of "
442                         + (minimumAmount == null ? "null" : minimumAmount.toString()) + ".");
443             }
444             return true;
445         }
446 
447         if (LOG.isInfoEnabled()) {
448             LOG.info(" -- PayReq ["
449                     + document.getDocumentNumber()
450                     + "] skipped due to document Total ["
451                     + document.getFinancialSystemDocumentHeader().getFinancialDocumentTotalAmount()
452                     + "] being greater than "
453                     + (minimumAmount == defaultMinimumLimit ? "Default Auto-Approval Limit "
454                     : "Configured Auto-Approval Limit ") + "of "
455                     + (minimumAmount == null ? "null" : minimumAmount.toString()) + ".");
456         }
457 
458         return false;
459     }
460 
461     /**
462      * This method iterates a collection of negative payment request approval limits and returns the minimum of a given minimum
463      * amount and the least among the limits in the collection.
464      *
465      * @param limits        The collection of NegativePaymentRequestApprovalLimit to be used in determining the minimum limit amount.
466      * @param minimumAmount The amount to be compared with the collection of NegativePaymentRequestApprovalLimit to determine the
467      *                      minimum limit amount.
468      * @return The minimum of the given minimum amount and the least among the limits in the collection.
469      */
470     protected KualiDecimal getMinimumLimitAmount(Collection<NegativePaymentRequestApprovalLimit> limits, KualiDecimal minimumAmount) {
471         for (NegativePaymentRequestApprovalLimit limit : limits) {
472             KualiDecimal amount = limit.getNegativePaymentRequestApprovalLimitAmount();
473             if (null == minimumAmount) {
474                 minimumAmount = amount;
475             } else if (minimumAmount.isGreaterThan(amount)) {
476                 minimumAmount = amount;
477             }
478         }
479         return minimumAmount;
480     }
481 
482     /**
483      * Retrieves a list of payment request documents with the given vendor id and invoice number.
484      *
485      * @param vendorHeaderGeneratedId The vendor header generated id.
486      * @param vendorDetailAssignedId  The vendor detail assigned id.
487      * @param invoiceNumber           The invoice number as entered by AP.
488      * @return List of payment request document.
489      */
490     @Override
491     public List getPaymentRequestsByVendorNumber(Integer vendorHeaderGeneratedId, Integer vendorDetailAssignedId) {
492         LOG.debug("getActivePaymentRequestsByVendorNumber() started");
493         return paymentRequestDao.getActivePaymentRequestsByVendorNumber(vendorHeaderGeneratedId, vendorDetailAssignedId);
494     }
495 
496     /**
497      * Retrieves a list of payment request documents with the given vendor id and invoice number.
498      *
499      * @param vendorHeaderGeneratedId The vendor header generated id.
500      * @param vendorDetailAssignedId  The vendor detail assigned id.
501      * @param invoiceNumber           The invoice number as entered by AP.
502      * @return List of payment request document.
503      */
504     @Override
505     public List getPaymentRequestsByVendorNumberInvoiceNumber(Integer vendorHeaderGeneratedId, Integer vendorDetailAssignedId, String invoiceNumber) {
506         LOG.debug("getActivePaymentRequestsByVendorNumberInvoiceNumber() started");
507         return paymentRequestDao.getActivePaymentRequestsByVendorNumberInvoiceNumber(vendorHeaderGeneratedId, vendorDetailAssignedId, invoiceNumber);
508     }
509 
510     /**
511      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#paymentRequestDuplicateMessages(org.kuali.ole.module.purap.document.PaymentRequestDocument)
512      */
513     @Override
514     public HashMap<String, String> paymentRequestDuplicateMessages(PaymentRequestDocument document) {
515         HashMap<String, String> msgs;
516         msgs = new HashMap<String, String>();
517 
518         Integer purchaseOrderId = document.getPurchaseOrderIdentifier();
519 
520         if (ObjectUtils.isNotNull(document.getInvoiceDate())) {
521             if (purapService.isDateAYearBeforeToday(document.getInvoiceDate())) {
522                 msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService.getPropertyValueAsString(PurapKeyConstants.MESSAGE_INVOICE_DATE_A_YEAR_OR_MORE_PAST));
523             }
524         }
525         PurchaseOrderDocument po = document.getPurchaseOrderDocument();
526 
527         if (po != null) {
528             Integer vendorDetailAssignedId = po.getVendorDetailAssignedIdentifier();
529             Integer vendorHeaderGeneratedId = po.getVendorHeaderGeneratedIdentifier();
530 
531             List<PaymentRequestDocument> preqs = new ArrayList();
532 
533             List<PaymentRequestDocument> preqsDuplicates = getPaymentRequestsByVendorNumber(vendorHeaderGeneratedId, vendorDetailAssignedId);
534             for (PaymentRequestDocument duplicatePREQ : preqsDuplicates) {
535                 if (duplicatePREQ!=null && duplicatePREQ.getInvoiceNumber()!=null &&
536                         document!=null && document.getInvoiceNumber()!=null &&
537                         duplicatePREQ.getInvoiceNumber().toUpperCase().equals(document.getInvoiceNumber().toUpperCase())) {
538                     // found the duplicate row... so add to the preqs list...
539                     preqs.add(duplicatePREQ);
540                 }
541             }
542 
543             if (preqs.size() > 0) {
544                 boolean addedMessage = false;
545                 boolean foundCanceledPostApprove = false; // cancelled
546                 boolean foundCanceledPreApprove = false; // voided
547                 for (PaymentRequestDocument testPREQ : preqs) {
548                     if (StringUtils.equals(testPREQ.getApplicationDocumentStatus(), PaymentRequestStatuses.APPDOC_CANCELLED_POST_AP_APPROVE)) {
549                         foundCanceledPostApprove |= true;
550                     } else if (StringUtils.equals(testPREQ.getApplicationDocumentStatus(), PaymentRequestStatuses.APPDOC_CANCELLED_IN_PROCESS)) {
551                         foundCanceledPreApprove |= true;
552                     } else {
553                         msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService.getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE));
554                         addedMessage = true;
555                         break;
556                     }
557                 }
558                 // Custom error message for duplicates related to cancelled/voided PREQs
559                 if (!addedMessage) {
560                     if (foundCanceledPostApprove && foundCanceledPreApprove) {
561                         msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService.getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_CANCELLEDORVOIDED));
562                     } else if (foundCanceledPreApprove) {
563                         msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService.getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_VOIDED));
564                     } else if (foundCanceledPostApprove) {
565                         // messages.add("errors.duplicate.vendor.invoice.cancelled");
566                         msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService.getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_CANCELLED));
567                     }
568                 }
569             }
570 
571             // Check that the invoice date and invoice total amount entered are not on any existing non-cancelled PREQs for this PO
572             preqs = getPaymentRequestsByPOIdInvoiceAmountInvoiceDate(purchaseOrderId, document.getVendorInvoiceAmount(), document.getInvoiceDate());
573             if (preqs.size() > 0) {
574                 boolean addedMessage = false;
575                 boolean foundCanceledPostApprove = false; // cancelled
576                 boolean foundCanceledPreApprove = false; // voided
577                 msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService.getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_DATE_AMOUNT));
578                 for (PaymentRequestDocument testPREQ : preqs) {
579                     if (StringUtils.equalsIgnoreCase(testPREQ.getApplicationDocumentStatus(), PaymentRequestStatuses.APPDOC_CANCELLED_POST_AP_APPROVE)) {
580                         foundCanceledPostApprove |= true;
581                     } else if (StringUtils.equalsIgnoreCase(testPREQ.getApplicationDocumentStatus(), PaymentRequestStatuses.APPDOC_CANCELLED_IN_PROCESS)) {
582                         foundCanceledPreApprove |= true;
583                     } else {
584                         msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService.getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_DATE_AMOUNT));
585                         addedMessage = true;
586                         break;
587                     }
588                 }
589 
590                 // Custom error message for duplicates related to cancelled/voided PREQs
591                 if (!addedMessage) {
592                     if (foundCanceledPostApprove && foundCanceledPreApprove) {
593                         msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService.getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_DATE_AMOUNT_CANCELLEDORVOIDED));
594                     } else if (foundCanceledPreApprove) {
595                         msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService.getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_DATE_AMOUNT_VOIDED));
596                         addedMessage = true;
597                     } else if (foundCanceledPostApprove) {
598                         msgs.put(PREQDocumentsStrings.DUPLICATE_INVOICE_QUESTION, configurationService.getPropertyValueAsString(PurapKeyConstants.MESSAGE_DUPLICATE_INVOICE_DATE_AMOUNT_CANCELLED));
599                         addedMessage = true;
600                     }
601 
602                 }
603             }
604         }
605         return msgs;
606     }
607 
608     /**
609      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#getPaymentRequestByDocumentNumber(java.lang.String)
610      */
611     @Override
612     public PaymentRequestDocument getPaymentRequestByDocumentNumber(String documentNumber) {
613         LOG.debug("getPaymentRequestByDocumentNumber() started");
614 
615         if (ObjectUtils.isNotNull(documentNumber)) {
616             try {
617                 PaymentRequestDocument doc = (PaymentRequestDocument) documentService.getByDocumentHeaderId(documentNumber);
618                 return doc;
619             } catch (WorkflowException e) {
620                 String errorMessage = "Error getting payment request document from document service";
621                 LOG.error("getPaymentRequestByDocumentNumber() " + errorMessage, e);
622                 throw new RuntimeException(errorMessage, e);
623             }
624         }
625         return null;
626     }
627 
628     /**
629      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#getPaymentRequestById(java.lang.Integer)
630      */
631     @Override
632     public PaymentRequestDocument getPaymentRequestById(Integer poDocId) {
633         return getPaymentRequestByDocumentNumber(paymentRequestDao.getDocumentNumberByPaymentRequestId(poDocId));
634     }
635 
636     /**
637      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#getPaymentRequestsByPurchaseOrderId(java.lang.Integer)
638      */
639     @Override
640     public List<PaymentRequestDocument> getPaymentRequestsByPurchaseOrderId(Integer poDocId) {
641         List<PaymentRequestDocument> preqs = new ArrayList<PaymentRequestDocument>();
642         List<String> docNumbers = paymentRequestDao.getDocumentNumbersByPurchaseOrderId(poDocId);
643         for (String docNumber : docNumbers) {
644             PaymentRequestDocument preq = getPaymentRequestByDocumentNumber(docNumber);
645             if (ObjectUtils.isNotNull(preq)) {
646                 preqs.add(preq);
647             }
648         }
649         return preqs;
650     }
651 
652     /**
653      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#getPaymentRequestsByPOIdInvoiceAmountInvoiceDate(java.lang.Integer,
654      *      org.kuali.rice.core.api.util.type.KualiDecimal, java.sql.Date)
655      */
656     @Override
657     public List<PaymentRequestDocument> getPaymentRequestsByPOIdInvoiceAmountInvoiceDate(Integer poId, KualiDecimal invoiceAmount, Date invoiceDate) {
658         LOG.debug("getPaymentRequestsByPOIdInvoiceAmountInvoiceDate() started");
659         return paymentRequestDao.getActivePaymentRequestsByPOIdInvoiceAmountInvoiceDate(poId, invoiceAmount, invoiceDate);
660     }
661 
662     /**
663      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#isInvoiceDateAfterToday(java.sql.Date)
664      */
665     @Override
666     public boolean isInvoiceDateAfterToday(Date invoiceDate) {
667         // Check invoice date to make sure it is today or before
668         Calendar now = Calendar.getInstance();
669         now.set(Calendar.HOUR, 11);
670         now.set(Calendar.MINUTE, 59);
671         now.set(Calendar.SECOND, 59);
672         now.set(Calendar.MILLISECOND, 59);
673         Timestamp nowTime = new Timestamp(now.getTimeInMillis());
674         Calendar invoiceDateC = Calendar.getInstance();
675         invoiceDateC.setTime(invoiceDate);
676         // set time to midnight
677         invoiceDateC.set(Calendar.HOUR, 0);
678         invoiceDateC.set(Calendar.MINUTE, 0);
679         invoiceDateC.set(Calendar.SECOND, 0);
680         invoiceDateC.set(Calendar.MILLISECOND, 0);
681         Timestamp invoiceDateTime = new Timestamp(invoiceDateC.getTimeInMillis());
682         return ((invoiceDateTime.compareTo(nowTime)) > 0);
683     }
684 
685     /**
686      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#calculatePayDate(java.sql.Date,
687      *      org.kuali.ole.vnd.businessobject.PaymentTermType)
688      */
689     @Override
690     public java.sql.Date calculatePayDate(Date invoiceDate, PaymentTermType terms) {
691         LOG.debug("calculatePayDate() started");
692         // calculate the invoice + processed calendar
693         Calendar invoicedDateCalendar = dateTimeService.getCalendar(invoiceDate);
694         Calendar processedDateCalendar = dateTimeService.getCurrentCalendar();
695 
696         // add default number of days to processed
697         String defaultDays = parameterService.getParameterValueAsString(PaymentRequestDocument.class, PurapParameterConstants.PURAP_PREQ_PAY_DATE_DEFAULT_NUMBER_OF_DAYS);
698         processedDateCalendar.add(Calendar.DAY_OF_MONTH, Integer.parseInt(defaultDays));
699 
700         if (ObjectUtils.isNull(terms) || StringUtils.isEmpty(terms.getVendorPaymentTermsCode())) {
701             invoicedDateCalendar.add(Calendar.DAY_OF_MONTH, PurapConstants.PREQ_PAY_DATE_EMPTY_TERMS_DEFAULT_DAYS);
702             return returnLaterDate(invoicedDateCalendar, processedDateCalendar);
703         }
704 
705         Integer discountDueNumber = terms.getVendorDiscountDueNumber();
706         Integer netDueNumber = terms.getVendorNetDueNumber();
707         if (ObjectUtils.isNotNull(discountDueNumber)) {
708             String discountDueTypeDescription = terms.getVendorDiscountDueTypeDescription();
709             paymentTermsDateCalculation(discountDueTypeDescription, invoicedDateCalendar, discountDueNumber);
710         } else if (ObjectUtils.isNotNull(netDueNumber)) {
711             String netDueTypeDescription = terms.getVendorNetDueTypeDescription();
712             paymentTermsDateCalculation(netDueTypeDescription, invoicedDateCalendar, netDueNumber);
713         } else {
714             throw new RuntimeException("Neither discount or net number were specified for this payment terms type");
715         }
716 
717         // return the later date
718         return returnLaterDate(invoicedDateCalendar, processedDateCalendar);
719     }
720 
721     /**
722      * Returns whichever date is later, the invoicedDateCalendar or the processedDateCalendar.
723      *
724      * @param invoicedDateCalendar  One of the dates to be used in determining which date is later.
725      * @param processedDateCalendar The other date to be used in determining which date is later.
726      * @return The date which is the later of the two given dates in the input parameters.
727      */
728     protected java.sql.Date returnLaterDate(Calendar invoicedDateCalendar, Calendar processedDateCalendar) {
729         if (invoicedDateCalendar.after(processedDateCalendar)) {
730             return new java.sql.Date(invoicedDateCalendar.getTimeInMillis());
731         } else {
732             return new java.sql.Date(processedDateCalendar.getTimeInMillis());
733         }
734     }
735 
736     /**
737      * Calculates the paymentTermsDate given the dueTypeDescription, invoicedDateCalendar and the dueNumber.
738      *
739      * @param dueTypeDescription   The due type description of the payment term.
740      * @param invoicedDateCalendar The Calendar object of the invoice date.
741      * @param discountDueNumber    Either the vendorDiscountDueNumber or the vendorDiscountDueNumber of the payment term.
742      */
743     protected void paymentTermsDateCalculation(String dueTypeDescription, Calendar invoicedDateCalendar, Integer dueNumber) {
744 
745         if (StringUtils.equals(dueTypeDescription, PurapConstants.PREQ_PAY_DATE_DATE)) {
746             // date specified set to date in next month
747             invoicedDateCalendar.add(Calendar.MONTH, 1);
748             invoicedDateCalendar.set(Calendar.DAY_OF_MONTH, dueNumber.intValue());
749         } else if (StringUtils.equals(PurapConstants.PREQ_PAY_DATE_DAYS, dueTypeDescription)) {
750             // days specified go forward that number
751             invoicedDateCalendar.add(Calendar.DAY_OF_MONTH, dueNumber.intValue());
752         } else {
753             // improper string
754             throw new RuntimeException("missing payment terms description or not properly enterred on payment term maintenance doc");
755         }
756     }
757 
758     /**
759      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#calculatePaymentRequest(org.kuali.ole.module.purap.document.PaymentRequestDocument,
760      *      boolean)
761      */
762     @Override
763     public void calculatePaymentRequest(PaymentRequestDocument paymentRequest, boolean updateDiscount) {
764         LOG.debug("calculatePaymentRequest() started");
765 
766         // general calculation, i.e. for the whole preq document
767         if (ObjectUtils.isNull(paymentRequest.getPaymentRequestPayDate())) {
768             paymentRequest.setPaymentRequestPayDate(calculatePayDate(paymentRequest.getInvoiceDate(), paymentRequest.getVendorPaymentTerms()));
769         }
770 
771         distributeAccounting(paymentRequest);
772 
773         purapService.calculateTax(paymentRequest);
774 
775         // do proration for full order and trade in
776         purapService.prorateForTradeInAndFullOrderDiscount(paymentRequest);
777 
778         // do proration for payment terms discount
779         if (updateDiscount) {
780             calculateDiscount(paymentRequest);
781         }
782 
783         distributeAccounting(paymentRequest);
784     }
785 
786 
787     /**
788      * Calculates the discount item for this paymentRequest.
789      *
790      * @param paymentRequestDocument The payment request document whose discount to be calculated.
791      */
792     protected void calculateDiscount(PaymentRequestDocument paymentRequestDocument) {
793         PaymentRequestItem discountItem = findDiscountItem(paymentRequestDocument);
794         // find out if we really need the discount item
795         PaymentTermType pt = paymentRequestDocument.getVendorPaymentTerms();
796         if ((pt != null) && (pt.getVendorPaymentTermsPercent() != null) && (BigDecimal.ZERO.compareTo(pt.getVendorPaymentTermsPercent()) != 0)) {
797             if (discountItem == null) {
798                 // set discountItem and add to items
799                 // this is probably not the best way of doing it but should work for now if we start excluding discount from below
800                 // we will need to manually add
801                 purapService.addBelowLineItems(paymentRequestDocument);
802 
803                 // fix up below the line items
804                 removeIneligibleAdditionalCharges(paymentRequestDocument);
805 
806                 discountItem = findDiscountItem(paymentRequestDocument);
807             }
808 
809             // Deleted the discountItem.getExtendedPrice() null and isZero
810             PaymentRequestItem fullOrderItem = findFullOrderDiscountItem(paymentRequestDocument);
811             KualiDecimal fullOrderAmount = KualiDecimal.ZERO;
812             KualiDecimal fullOrderTaxAmount = KualiDecimal.ZERO;
813 
814             if (fullOrderItem != null) {
815                 fullOrderAmount = (ObjectUtils.isNotNull(fullOrderItem.getExtendedPrice())) ? fullOrderItem.getExtendedPrice() : KualiDecimal.ZERO;
816                 fullOrderTaxAmount = (ObjectUtils.isNotNull(fullOrderItem.getItemTaxAmount())) ? fullOrderItem.getItemTaxAmount() : KualiDecimal.ZERO;
817             }
818             KualiDecimal totalCost = paymentRequestDocument.getTotalPreTaxDollarAmountAboveLineItems().add(fullOrderAmount);
819             PurApItem tradeInItem = paymentRequestDocument.getTradeInItem();
820             if (ObjectUtils.isNotNull(tradeInItem)) {
821                 totalCost = totalCost.subtract(tradeInItem.getTotalAmount());
822             }
823             BigDecimal discountAmount = pt.getVendorPaymentTermsPercent().multiply(totalCost.bigDecimalValue()).multiply(new BigDecimal(PurapConstants.PREQ_DISCOUNT_MULT));
824 
825             // do we really need to set both, not positive, but probably won't hurt
826             discountItem.setItemUnitPrice(discountAmount.setScale(2, KualiDecimal.ROUND_BEHAVIOR));
827             discountItem.setExtendedPrice(new KualiDecimal(discountAmount));
828 
829             // set tax amount
830             boolean salesTaxInd = parameterService.getParameterValueAsBoolean(OleParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.ENABLE_SALES_TAX_IND);
831             boolean useTaxIndicator = paymentRequestDocument.isUseTaxIndicator();
832 
833             if (salesTaxInd == true && useTaxIndicator == false) {
834                 KualiDecimal totalTax = paymentRequestDocument.getTotalTaxAmountAboveLineItems().add(fullOrderTaxAmount);
835                 BigDecimal discountTaxAmount = null;
836                 if (totalCost.isNonZero()) {
837                     discountTaxAmount = discountAmount.divide(totalCost.bigDecimalValue()).multiply(totalTax.bigDecimalValue());
838                 } else {
839                     discountTaxAmount = BigDecimal.ZERO;
840                 }
841 
842                 discountItem.setItemTaxAmount(new KualiDecimal(discountTaxAmount.setScale(KualiDecimal.SCALE, KualiDecimal.ROUND_BEHAVIOR)));
843             }
844 
845             // set document
846             discountItem.setPurapDocument(paymentRequestDocument);
847         } else { // no discount
848             if (discountItem != null) {
849                 paymentRequestDocument.getItems().remove(discountItem);
850             }
851         }
852 
853     }
854 
855 
856     @Override
857     public void clearTax(PaymentRequestDocument document) {
858         // remove all existing tax items added by previous calculation
859         removeTaxItems(document);
860         // reset values
861         document.setTaxClassificationCode(null);
862         document.setTaxFederalPercent(null);
863         document.setTaxStatePercent(null);
864         document.setTaxCountryCode(null);
865         document.setTaxNQIId(null);
866 
867         document.setTaxForeignSourceIndicator(false);
868         document.setTaxExemptTreatyIndicator(false);
869         document.setTaxOtherExemptIndicator(false);
870         document.setTaxGrossUpIndicator(false);
871         document.setTaxUSAIDPerDiemIndicator(false);
872         document.setTaxSpecialW4Amount(null);
873 
874     }
875 
876     /**
877      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#calculateTaxArea(org.kuali.ole.module.purap.document.PaymentRequestDocument)
878      */
879     @Override
880     public void calculateTaxArea(PaymentRequestDocument preq) {
881         LOG.debug("calculateTaxArea() started");
882 
883         // remove all existing tax items added by previous calculation
884         removeTaxItems(preq);
885 
886         // don't need to calculate tax items if TaxClassificationCode is N (Non_Reportable)
887         if (StringUtils.equalsIgnoreCase(preq.getTaxClassificationCode(), "N")) {
888             return;
889         }
890 
891         // reserve the grand total excluding any tax amount, to be used as the base to compute all tax items
892         // if we don't reserve this, the pre-tax total could be changed as new tax items are added
893         BigDecimal taxableAmount = preq.getGrandPreTaxTotal().bigDecimalValue();
894 
895         // generate and add state tax gross up item and its accounting line, update total amount,
896         // if gross up indicator is true and tax rate is non-zero
897         if (preq.getTaxGrossUpIndicator() && preq.getTaxStatePercent().compareTo(new BigDecimal(0)) != 0) {
898             PurApItem stateGrossItem = addTaxItem(preq, ItemTypeCodes.ITEM_TYPE_STATE_GROSS_CODE, taxableAmount);
899         }
900 
901         // generate and add state tax item and its accounting line, update total amount, if tax rate is non-zero
902         if (preq.getTaxStatePercent().compareTo(new BigDecimal(0)) != 0) {
903             PurApItem stateTaxItem = addTaxItem(preq, ItemTypeCodes.ITEM_TYPE_STATE_TAX_CODE, taxableAmount);
904         }
905 
906         // generate and add federal tax gross up item and its accounting line, update total amount,
907         // if gross up indicator is true and tax rate is non-zero
908         if (preq.getTaxGrossUpIndicator() && preq.getTaxFederalPercent().compareTo(new BigDecimal(0)) != 0) {
909             PurApItem federalGrossItem = addTaxItem(preq, ItemTypeCodes.ITEM_TYPE_FEDERAL_GROSS_CODE, taxableAmount);
910         }
911 
912         // generate and add federal tax item and its accounting line, update total amount, if tax rate is non-zero
913         if (preq.getTaxFederalPercent().compareTo(new BigDecimal(0)) != 0) {
914             PurApItem federalTaxItem = addTaxItem(preq, ItemTypeCodes.ITEM_TYPE_FEDERAL_TAX_CODE, taxableAmount);
915         }
916 
917         // FIXME if user request to add zero tax lines and remove them after tax approval,
918         // then remove the conditions above when adding the tax lines, and
919         // add a branch in PaymentRequestDocument.processNodeChange to call PurapService.deleteUnenteredItems
920     }
921 
922     /**
923      * Removes all existing NRA tax items from the specified payment request.
924      *
925      * @param preq The payment request from which all tax items are to be removed.
926      */
927     protected void removeTaxItems(PaymentRequestDocument preq) {
928         List<PurApItem> items = preq.getItems();
929         for (int i = 0; i < items.size(); i++) {
930             PurApItem item = items.get(i);
931             String code = item.getItemTypeCode();
932             if (ItemTypeCodes.ITEM_TYPE_FEDERAL_TAX_CODE.equals(code) || ItemTypeCodes.ITEM_TYPE_STATE_TAX_CODE.equals(code) || ItemTypeCodes.ITEM_TYPE_FEDERAL_GROSS_CODE.equals(code) || ItemTypeCodes.ITEM_TYPE_STATE_GROSS_CODE.equals(code)) {
933                 items.remove(i--);
934             }
935         }
936     }
937 
938     /**
939      * Generates a NRA tax item and adds to the specified payment request, according to the specified item type code.
940      *
941      * @param preq          The payment request the tax item will be added to.
942      * @param itemTypeCode  The item type code for the tax item.
943      * @param taxableAmount The amount to which tax is computed against.
944      * @return A fully populated PurApItem instance representing NRA tax amount data for the specified payment request.
945      */
946     protected PurApItem addTaxItem(PaymentRequestDocument preq, String itemTypeCode, BigDecimal taxableAmount) {
947         PurApItem taxItem = null;
948 
949         try {
950             taxItem = (PurApItem) preq.getItemClass().newInstance();
951         } catch (IllegalAccessException e) {
952             throw new IllegalArgumentException("Unable to access itemClass", e);
953         } catch (InstantiationException e) {
954             throw new IllegalArgumentException("Unable to instantiate itemClass", e);
955         }
956 
957         // add item to preq before adding the accounting line
958         taxItem.setItemTypeCode(itemTypeCode);
959         preq.addItem(taxItem);
960 
961         // generate and add tax accounting line
962         PurApAccountingLine taxLine = addTaxAccountingLine(taxItem, taxableAmount);
963 
964         // set extended price amount as now it's calculated when accounting line is generated
965         taxItem.setItemUnitPrice(taxLine.getAmount().bigDecimalValue());
966         taxItem.setExtendedPrice(taxLine.getAmount());
967 
968         // use item type description as the item description
969         ItemType itemType = new ItemType();
970         itemType.setItemTypeCode(itemTypeCode);
971         itemType = (ItemType) businessObjectService.retrieve(itemType);
972         taxItem.setItemType(itemType);
973         taxItem.setItemDescription(itemType.getItemTypeDescription());
974 
975         return taxItem;
976     }
977 
978     /**
979      * Generates a PurAP accounting line and adds to the specified tax item.
980      *
981      * @param taxItem       The specified tax item the accounting line will be associated with.
982      * @param taxableAmount The amount to which tax is computed against.
983      * @return A fully populated PurApAccountingLine instance for the specified tax item.
984      */
985     protected PurApAccountingLine addTaxAccountingLine(PurApItem taxItem, BigDecimal taxableAmount) {
986         PaymentRequestDocument preq = taxItem.getPurapDocument();
987         PurApAccountingLine taxLine = null;
988 
989         try {
990             taxLine = (PurApAccountingLine) taxItem.getAccountingLineClass().newInstance();
991         } catch (IllegalAccessException e) {
992             throw new IllegalArgumentException("Unable to access sourceAccountingLineClass", e);
993         } catch (InstantiationException e) {
994             throw new IllegalArgumentException("Unable to instantiate sourceAccountingLineClass", e);
995         }
996 
997         // tax item type indicators
998         boolean isFederalTax = ItemTypeCodes.ITEM_TYPE_FEDERAL_TAX_CODE.equals(taxItem.getItemTypeCode());
999         boolean isFederalGross = ItemTypeCodes.ITEM_TYPE_FEDERAL_GROSS_CODE.equals(taxItem.getItemTypeCode());
1000         boolean isStateTax = ItemTypeCodes.ITEM_TYPE_STATE_TAX_CODE.equals(taxItem.getItemTypeCode());
1001         boolean isStateGross = ItemTypeCodes.ITEM_TYPE_STATE_GROSS_CODE.equals(taxItem.getItemTypeCode());
1002         boolean isFederal = isFederalTax || isFederalGross; // true for federal tax/gross; false for state tax/gross
1003         boolean isGross = isFederalGross || isStateGross; // true for federal/state gross, false for federal/state tax
1004 
1005         // obtain accounting line info according to tax item type code
1006         String taxChart = null;
1007         String taxAccount = null;
1008         String taxObjectCode = null;
1009 
1010         if (isGross) {
1011             // for gross up tax items, use preq's first item's first accounting line, which shall exist at this point
1012             AccountingLine line1 = preq.getFirstAccount();
1013             taxChart = line1.getChartOfAccountsCode();
1014             taxAccount = line1.getAccountNumber();
1015             taxObjectCode = line1.getFinancialObjectCode();
1016         } else if (isFederalTax) {
1017             // for federal tax item, get chart, account, object code info from parameters
1018             taxChart = parameterService.getParameterValueAsString(PaymentRequestDocument.class, NRATaxParameters.FEDERAL_TAX_PARM_PREFIX + NRATaxParameters.TAX_PARM_CHART_SUFFIX);
1019             taxAccount = parameterService.getParameterValueAsString(PaymentRequestDocument.class, NRATaxParameters.FEDERAL_TAX_PARM_PREFIX + NRATaxParameters.TAX_PARM_ACCOUNT_SUFFIX);
1020             taxObjectCode = parameterService.getSubParameterValueAsString(PaymentRequestDocument.class, NRATaxParameters.FEDERAL_TAX_PARM_PREFIX + NRATaxParameters.TAX_PARM_OBJECT_BY_INCOME_CLASS_SUFFIX, preq.getTaxClassificationCode());
1021             if (StringUtils.isBlank(taxChart) || StringUtils.isBlank(taxAccount) || StringUtils.isBlank(taxObjectCode)) {
1022                 LOG.error("Unable to retrieve federal tax parameters.");
1023                 throw new RuntimeException("Unable to retrieve federal tax parameters.");
1024             }
1025         } else if (isStateTax) {
1026             // for state tax item, get chart, account, object code info from parameters
1027             taxChart = parameterService.getParameterValueAsString(PaymentRequestDocument.class, NRATaxParameters.STATE_TAX_PARM_PREFIX + NRATaxParameters.TAX_PARM_CHART_SUFFIX);
1028             taxAccount = parameterService.getParameterValueAsString(PaymentRequestDocument.class, NRATaxParameters.STATE_TAX_PARM_PREFIX + NRATaxParameters.TAX_PARM_ACCOUNT_SUFFIX);
1029             taxObjectCode = parameterService.getSubParameterValueAsString(PaymentRequestDocument.class, NRATaxParameters.STATE_TAX_PARM_PREFIX + NRATaxParameters.TAX_PARM_OBJECT_BY_INCOME_CLASS_SUFFIX, preq.getTaxClassificationCode());
1030             if (StringUtils.isBlank(taxChart) || StringUtils.isBlank(taxAccount) || StringUtils.isBlank(taxObjectCode)) {
1031                 LOG.error("Unable to retrieve state tax parameters.");
1032                 throw new RuntimeException("Unable to retrieve state tax parameters.");
1033             }
1034         }
1035 
1036         // calculate tax amount according to gross up indicator and federal/state tax type
1037         /*
1038          * The formula of tax and gross up amount are as follows: if (not gross up) gross not existing taxFederal/State = - amount *
1039          * rateFederal/State otherwise gross up grossFederal/State = amount * rateFederal/State / (1 - rateFederal - rateState) tax
1040          * = - gross
1041          */
1042 
1043         // pick federal/state tax rate
1044         BigDecimal taxPercentFederal = preq.getTaxFederalPercent();
1045         BigDecimal taxPercentState = preq.getTaxStatePercent();
1046         BigDecimal taxPercent = isFederal ? taxPercentFederal : taxPercentState;
1047 
1048         // divider value according to gross up or not
1049         BigDecimal taxDivider = new BigDecimal(100);
1050         if (preq.getTaxGrossUpIndicator()) {
1051             taxDivider = taxDivider.subtract(taxPercentFederal.add(taxPercentState));
1052         }
1053 
1054         // tax = amount * rate / divider
1055         BigDecimal taxAmount = taxableAmount.multiply(taxPercent);
1056         taxAmount = taxAmount.divide(taxDivider, 5, BigDecimal.ROUND_HALF_UP);
1057 
1058         // tax is always negative, since it reduces the total amount; while gross up is always the positive of tax
1059         if (!isGross) {
1060             taxAmount = taxAmount.negate();
1061         }
1062 
1063         // populate necessary accounting line fields
1064         taxLine.setDocumentNumber(preq.getDocumentNumber());
1065         taxLine.setSequenceNumber(preq.getNextSourceLineNumber());
1066         taxLine.setChartOfAccountsCode(taxChart);
1067         taxLine.setAccountNumber(taxAccount);
1068         taxLine.setFinancialObjectCode(taxObjectCode);
1069         taxLine.setAmount(new KualiDecimal(taxAmount));
1070 
1071         // add the accounting line to the item
1072         taxLine.setItemIdentifier(taxItem.getItemIdentifier());
1073         taxLine.setPurapItem(taxItem);
1074         taxItem.getSourceAccountingLines().add(taxLine);
1075 
1076         return taxLine;
1077     }
1078 
1079     /**
1080      * Finds the discount item of the payment request document.
1081      *
1082      * @param paymentRequestDocument The payment request document to be used to find the discount item.
1083      * @return The discount item if it exists.
1084      */
1085     protected PaymentRequestItem findDiscountItem(PaymentRequestDocument paymentRequestDocument) {
1086         PaymentRequestItem discountItem = null;
1087         for (PaymentRequestItem preqItem : (List<PaymentRequestItem>) paymentRequestDocument.getItems()) {
1088             if (StringUtils.equals(preqItem.getItemTypeCode(), PurapConstants.ItemTypeCodes.ITEM_TYPE_PMT_TERMS_DISCOUNT_CODE)) {
1089                 discountItem = preqItem;
1090                 break;
1091             }
1092         }
1093         return discountItem;
1094     }
1095 
1096     /**
1097      * Finds the full order discount item of the payment request document.
1098      *
1099      * @param paymentRequestDocument The payment request document to be used to find the full order discount item.
1100      * @return The discount item if it exists.
1101      */
1102     protected PaymentRequestItem findFullOrderDiscountItem(PaymentRequestDocument paymentRequestDocument) {
1103         PaymentRequestItem discountItem = null;
1104         for (PaymentRequestItem preqItem : (List<PaymentRequestItem>) paymentRequestDocument.getItems()) {
1105             if (StringUtils.equals(preqItem.getItemTypeCode(), PurapConstants.ItemTypeCodes.ITEM_TYPE_ORDER_DISCOUNT_CODE)) {
1106                 discountItem = preqItem;
1107                 break;
1108             }
1109         }
1110         return discountItem;
1111     }
1112 
1113     /**
1114      * Distributes accounts for a payment request document.
1115      *
1116      * @param paymentRequestDocument
1117      */
1118     protected void distributeAccounting(PaymentRequestDocument paymentRequestDocument) {
1119         // update the account amounts before doing any distribution
1120         purapAccountingService.updateAccountAmounts(paymentRequestDocument);
1121 
1122         String accountDistributionMethod = paymentRequestDocument.getAccountDistributionMethod();
1123 
1124         for (PaymentRequestItem item : (List<PaymentRequestItem>) paymentRequestDocument.getItems()) {
1125             KualiDecimal totalAmount = KualiDecimal.ZERO;
1126             List<PurApAccountingLine> distributedAccounts = null;
1127             List<SourceAccountingLine> summaryAccounts = null;
1128             Set excludedItemTypeCodes = new HashSet();
1129             excludedItemTypeCodes.add(PurapConstants.ItemTypeCodes.ITEM_TYPE_PMT_TERMS_DISCOUNT_CODE);
1130 
1131             // skip above the line
1132             if (item.getItemType().isLineItemIndicator()) {
1133                 continue;
1134             }
1135 
1136             if ((item.getSourceAccountingLines().isEmpty()) && (ObjectUtils.isNotNull(item.getExtendedPrice())) && (KualiDecimal.ZERO.compareTo(item.getExtendedPrice()) != 0)) {
1137                 if ((StringUtils.equals(PurapConstants.ItemTypeCodes.ITEM_TYPE_PMT_TERMS_DISCOUNT_CODE, item.getItemType().getItemTypeCode())) && (paymentRequestDocument.getGrandTotal() != null) && ((KualiDecimal.ZERO.compareTo(paymentRequestDocument.getGrandTotal()) != 0))) {
1138 
1139                     // No discount is applied to other item types other than item line
1140                     // See KFSMI-5210 for details
1141 
1142                     // total amount should be the line item total, not the grand total
1143                     totalAmount = paymentRequestDocument.getLineItemTotal();
1144 
1145                     // prorate item line accounts only
1146                     Set includedItemTypeCodes = new HashSet();
1147                     includedItemTypeCodes.add(PurapConstants.ItemTypeCodes.ITEM_TYPE_ITEM_CODE);
1148                     includedItemTypeCodes.add(PurapConstants.ItemTypeCodes.ITEM_TYPE_SERVICE_CODE);
1149 
1150                     summaryAccounts = purapAccountingService.generateSummaryIncludeItemTypesAndNoZeroTotals(paymentRequestDocument.getItems(), includedItemTypeCodes);
1151                     //if summaryAccount is empty then do not call generateAccountDistributionForProration as
1152                     //there is a check in that method to throw NPE if accounts percents == 0..
1153                     //OLEMI-8487
1154                     if (summaryAccounts != null) {
1155                         distributedAccounts = purapAccountingService.generateAccountDistributionForProration(summaryAccounts, totalAmount, PurapConstants.PRORATION_SCALE, PaymentRequestAccount.class);
1156                     }
1157                     // OLE-3405 : disabling the distribution method choice
1158 //                    if (PurapConstants.AccountDistributionMethodCodes.SEQUENTIAL_CODE.equalsIgnoreCase(accountDistributionMethod)) {
1159 //                        purapAccountingService.updatePreqAccountAmountsWithTotal(distributedAccounts, item.getTotalAmount());
1160 //
1161 //                    } else {
1162 //
1163 //                        boolean rulePassed = true;
1164 //                        // check any business rules
1165 //                        rulePassed &= kualiRuleService.applyRules(new PurchasingAccountsPayableItemPreCalculateEvent(paymentRequestDocument, item));
1166 //
1167 //                        if (rulePassed) {
1168 //                            purapAccountingService.updatePreqProporationalAccountAmountsWithTotal(distributedAccounts, item.getTotalAmount());
1169 //                        }
1170 //                    }
1171                 } else {
1172                     PurchaseOrderItem poi = item.getPurchaseOrderItem();
1173                     if ((poi != null) && (poi.getSourceAccountingLines() != null) && (!(poi.getSourceAccountingLines().isEmpty())) && (poi.getExtendedPrice() != null) && ((KualiDecimal.ZERO.compareTo(poi.getExtendedPrice())) != 0)) {
1174                         // use accounts from purchase order item matching this item
1175                         // account list of current item is already empty
1176                         item.generateAccountListFromPoItemAccounts(poi.getSourceAccountingLines());
1177                     } else {
1178                         totalAmount = paymentRequestDocument.getPurchaseOrderDocument().getTotalDollarAmountAboveLineItems();
1179                         purapAccountingService.updateAccountAmounts(paymentRequestDocument.getPurchaseOrderDocument());
1180                         summaryAccounts = purapAccountingService.generateSummary(PurApItemUtils.getAboveTheLineOnly(paymentRequestDocument.getPurchaseOrderDocument().getItems()));
1181                         distributedAccounts = purapAccountingService.generateAccountDistributionForProration(summaryAccounts, totalAmount, new Integer("6"), PaymentRequestAccount.class);
1182                     }
1183                 }
1184                 if (CollectionUtils.isNotEmpty(distributedAccounts) && CollectionUtils.isEmpty(item.getSourceAccountingLines())) {
1185                     item.setSourceAccountingLines(distributedAccounts);
1186                 }
1187             }
1188             // update the item
1189             purapAccountingService.updateItemAccountAmounts(item);
1190         }
1191 
1192         // update again now that distribute is finished. (Note: we may not need this anymore now that I added updateItem line above
1193         //leave the call below since we need to this when sequential method is used on the document.
1194         purapAccountingService.updateAccountAmounts(paymentRequestDocument);
1195     }
1196 
1197     /**
1198      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#addHoldOnPaymentRequest(org.kuali.ole.module.purap.document.PaymentRequestDocument,
1199      *      java.lang.String)
1200      */
1201     @Override
1202     public PaymentRequestDocument addHoldOnPaymentRequest(PaymentRequestDocument document, String note) throws Exception {
1203         // save the note
1204         Note noteObj = documentService.createNoteFromDocument(document, note);
1205         document.addNote(noteObj);
1206         noteService.save(noteObj);
1207 
1208         // MSU Contribution OLEMI-8456 DT 3822 OLECNTRB-959
1209 
1210         document.setHoldIndicator(true);
1211         document.setLastActionPerformedByPersonId(GlobalVariables.getUserSession().getPerson().getPrincipalId());
1212         purapService.saveDocumentNoValidation(document);
1213 
1214         return document;
1215     }
1216 
1217     /**
1218      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#removeHoldOnPaymentRequest(org.kuali.ole.module.purap.document.PaymentRequestDocument)
1219      */
1220     @Override
1221     public PaymentRequestDocument removeHoldOnPaymentRequest(PaymentRequestDocument document, String note) throws Exception {
1222         // save the note
1223         Note noteObj = documentService.createNoteFromDocument(document, note);
1224         document.addNote(noteObj);
1225         noteService.save(noteObj);
1226 
1227         // MSU Contribution OLEMI-8456 DT 3822 OLECNTRB-959
1228 
1229         document.setHoldIndicator(false);
1230         document.setLastActionPerformedByPersonId(null);
1231         purapService.saveDocumentNoValidation(document);
1232 
1233         return document;
1234     }
1235 
1236     /**
1237      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#addHoldOnPaymentRequest(org.kuali.ole.module.purap.document.PaymentRequestDocument,
1238      *      java.lang.String)
1239      */
1240     @Override
1241     public void requestCancelOnPaymentRequest(PaymentRequestDocument document, String note) throws Exception {
1242         // save the note
1243         Note noteObj = documentService.createNoteFromDocument(document, note);
1244         document.addNote(noteObj);
1245         noteService.save(noteObj);
1246 
1247         // MSU Contribution OLEMI-8456 DT 3822 OLECNTRB-959
1248         document.setPaymentRequestedCancelIndicator(true);
1249         document.setLastActionPerformedByPersonId(GlobalVariables.getUserSession().getPerson().getPrincipalId());
1250         document.setAccountsPayableRequestCancelIdentifier(GlobalVariables.getUserSession().getPerson().getPrincipalId());
1251         purapService.saveDocumentNoValidation(document);
1252     }
1253 
1254     /**
1255      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#removeHoldOnPaymentRequest(org.kuali.ole.module.purap.document.PaymentRequestDocument)
1256      */
1257     @Override
1258     public void removeRequestCancelOnPaymentRequest(PaymentRequestDocument document, String note) throws Exception {
1259         // save the note
1260         Note noteObj = documentService.createNoteFromDocument(document, note);
1261         document.addNote(noteObj);
1262         noteService.save(noteObj);
1263 
1264         clearRequestCancelFields(document);
1265 
1266         purapService.saveDocumentNoValidation(document);
1267 
1268     }
1269 
1270     /**
1271      * Clears the request cancel fields.
1272      *
1273      * @param document The payment request document whose request cancel fields to be cleared.
1274      */
1275     protected void clearRequestCancelFields(PaymentRequestDocument document) {
1276         document.setPaymentRequestedCancelIndicator(false);
1277         document.setLastActionPerformedByPersonId(null);
1278         document.setAccountsPayableRequestCancelIdentifier(null);
1279     }
1280 
1281     /**
1282      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#isExtracted(org.kuali.ole.module.purap.document.PaymentRequestDocument)
1283      */
1284     @Override
1285     public boolean isExtracted(PaymentRequestDocument document) {
1286         return (ObjectUtils.isNull(document.getExtractedTimestamp()) ? false : true);
1287     }
1288 
1289     protected boolean isBeingAdHocRouted(PaymentRequestDocument document) {
1290         return financialSystemWorkflowHelperService.isAdhocApprovalRequestedForPrincipal(document.getDocumentHeader().getWorkflowDocument(), GlobalVariables.getUserSession().getPrincipalId());
1291     }
1292 
1293     /**
1294      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#cancelExtractedPaymentRequest(org.kuali.ole.module.purap.document.PaymentRequestDocument,
1295      *      java.lang.String)
1296      */
1297     @Override
1298     public void cancelExtractedPaymentRequest(PaymentRequestDocument paymentRequest, String note) {
1299         LOG.debug("cancelExtractedPaymentRequest() started");
1300         if (PaymentRequestStatuses.CANCELLED_STATUSES.contains(paymentRequest.getApplicationDocumentStatus())) {
1301             LOG.debug("cancelExtractedPaymentRequest() ended");
1302             return;
1303         }
1304 
1305         try {
1306             Note cancelNote = documentService.createNoteFromDocument(paymentRequest, note);
1307             paymentRequest.addNote(cancelNote);
1308             noteService.save(cancelNote);
1309         } catch (Exception e) {
1310             throw new RuntimeException(PurapConstants.REQ_UNABLE_TO_CREATE_NOTE, e);
1311         }
1312 
1313         // cancel extracted should not reopen PO
1314         paymentRequest.setReopenPurchaseOrderIndicator(false);
1315 
1316         getAccountsPayableService().cancelAccountsPayableDocument(paymentRequest, ""); // Performs save, so
1317         // no explicit save
1318         // is necessary
1319         if (LOG.isDebugEnabled()) {
1320             LOG.debug("cancelExtractedPaymentRequest() PREQ " + paymentRequest.getPurapDocumentIdentifier() + " Cancelled Without Workflow");
1321             LOG.debug("cancelExtractedPaymentRequest() ended");
1322         }
1323     }
1324 
1325     /**
1326      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#resetExtractedPaymentRequest(org.kuali.ole.module.purap.document.PaymentRequestDocument,
1327      *      java.lang.String)
1328      */
1329     @Override
1330     public void resetExtractedPaymentRequest(PaymentRequestDocument paymentRequest, String note) {
1331         LOG.debug("resetExtractedPaymentRequest() started");
1332         if (PaymentRequestStatuses.CANCELLED_STATUSES.contains(paymentRequest.getApplicationDocumentStatus())) {
1333             LOG.debug("resetExtractedPaymentRequest() ended");
1334             return;
1335         }
1336         paymentRequest.setExtractedTimestamp(null);
1337         paymentRequest.setPaymentPaidTimestamp(null);
1338         String noteText = "This Payment Request is being reset for extraction by PDP " + note;
1339         try {
1340             Note resetNote = documentService.createNoteFromDocument(paymentRequest, noteText);
1341             paymentRequest.addNote(resetNote);
1342             noteService.save(resetNote);
1343         } catch (Exception e) {
1344             throw new RuntimeException(PurapConstants.REQ_UNABLE_TO_CREATE_NOTE + " " + e);
1345         }
1346         purapService.saveDocumentNoValidation(paymentRequest);
1347         if (LOG.isDebugEnabled()) {
1348             LOG.debug("resetExtractedPaymentRequest() PREQ " + paymentRequest.getPurapDocumentIdentifier() + " Reset from Extracted status");
1349         }
1350     }
1351 
1352     /**
1353      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#populatePaymentRequest(org.kuali.ole.module.purap.document.PaymentRequestDocument)
1354      */
1355     @Override
1356     public void populatePaymentRequest(PaymentRequestDocument paymentRequestDocument) {
1357 
1358         PurchaseOrderDocument purchaseOrderDocument = paymentRequestDocument.getPurchaseOrderDocument();
1359 
1360         // make a call to search for expired/closed accounts
1361         HashMap<String, ExpiredOrClosedAccountEntry> expiredOrClosedAccountList = getAccountsPayableService().getExpiredOrClosedAccountList(paymentRequestDocument);
1362         if (paymentRequestDocument.getInvoiceIdentifier() == null) {
1363             paymentRequestDocument.populatePaymentRequestFromPurchaseOrder(purchaseOrderDocument, expiredOrClosedAccountList);
1364         }
1365 
1366 
1367         paymentRequestDocument.getDocumentHeader().setDocumentDescription(createPreqDocumentDescription(paymentRequestDocument.getPurchaseOrderIdentifier(), paymentRequestDocument.getVendorName()));
1368 
1369         // write a note for expired/closed accounts if any exist and add a message stating there were expired/closed accounts at the
1370         // top of the document
1371         getAccountsPayableService().generateExpiredOrClosedAccountNote(paymentRequestDocument, expiredOrClosedAccountList);
1372 
1373         // set indicator so a message is displayed for accounts that were replaced due to expired/closed status
1374         if (!expiredOrClosedAccountList.isEmpty()) {
1375             paymentRequestDocument.setContinuationAccountIndicator(true);
1376         }
1377 
1378         // add discount item
1379         calculateDiscount(paymentRequestDocument);
1380         // distribute accounts (i.e. proration)
1381         distributeAccounting(paymentRequestDocument);
1382 
1383         // set bank code to default bank code in the system parameter
1384         Bank defaultBank = bankService.getDefaultBankByDocType(paymentRequestDocument.getClass());
1385         if (defaultBank != null) {
1386             paymentRequestDocument.setBankCode(defaultBank.getBankCode());
1387             paymentRequestDocument.setBank(defaultBank);
1388         }
1389     }
1390 
1391     /**
1392      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#createPreqDocumentDescription(java.lang.Integer,
1393      *      java.lang.String)
1394      */
1395     @Override
1396     public String createPreqDocumentDescription(Integer purchaseOrderIdentifier, String vendorName) {
1397         StringBuffer descr = new StringBuffer("");
1398         descr.append("PO: ");
1399         descr.append(purchaseOrderIdentifier);
1400         descr.append(" Vendor: ");
1401         descr.append(StringUtils.trimToEmpty(vendorName));
1402 
1403         int noteTextMaxLength = dataDictionaryService.getAttributeMaxLength(DocumentHeader.class, KRADPropertyConstants.DOCUMENT_DESCRIPTION).intValue();
1404         if (noteTextMaxLength >= descr.length()) {
1405             return descr.toString();
1406         } else {
1407             return descr.toString().substring(0, noteTextMaxLength);
1408         }
1409     }
1410 
1411     /**
1412      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#populateAndSavePaymentRequest(org.kuali.ole.module.purap.document.PaymentRequestDocument)
1413      */
1414     @Override
1415     public void populateAndSavePaymentRequest(PaymentRequestDocument preq) throws WorkflowException {
1416         try {
1417             preq.updateAndSaveAppDocStatus(PurapConstants.PaymentRequestStatuses.APPDOC_IN_PROCESS);
1418             documentService.saveDocument(preq, AttributedContinuePurapEvent.class);
1419             LOG.info("Payment request saved successfully" + preq.getDocumentNumber());
1420         } catch (ValidationException ve) {
1421             preq.updateAndSaveAppDocStatus(PurapConstants.PaymentRequestStatuses.APPDOC_INITIATE);
1422         } catch (WorkflowException we) {
1423             preq.updateAndSaveAppDocStatus(PurapConstants.PaymentRequestStatuses.APPDOC_INITIATE);
1424 
1425             String errorMsg = "Error saving document # " + preq.getDocumentHeader().getDocumentNumber() + " " + we.getMessage();
1426             LOG.error(errorMsg, we);
1427             throw new RuntimeException(errorMsg, we);
1428         }
1429     }
1430 
1431     /**
1432      * If the full document entry has been completed and the status of the related purchase order document is closed, return true,
1433      * otherwise return false.
1434      *
1435      * @param apDoc The AccountsPayableDocument to be determined whether its purchase order should be reversed.
1436      * @return boolean true if the purchase order should be reversed.
1437      * @see org.kuali.ole.module.purap.document.service.AccountsPayableDocumentSpecificService#shouldPurchaseOrderBeReversed
1438      *      (org.kuali.ole.module.purap.document.AccountsPayableDocument)
1439      */
1440     @Override
1441     public boolean shouldPurchaseOrderBeReversed(AccountsPayableDocument apDoc) {
1442         PurchaseOrderDocument po = apDoc.getPurchaseOrderDocument();
1443         if (ObjectUtils.isNull(po)) {
1444             throw new RuntimeException("po should never be null on PREQ");
1445         }
1446         // if past full entry and already closed return true
1447         if (purapService.isFullDocumentEntryCompleted(apDoc) && StringUtils.equalsIgnoreCase(PurapConstants.PurchaseOrderStatuses.APPDOC_CLOSED, po.getApplicationDocumentStatus())) {
1448             return true;
1449         }
1450         return false;
1451     }
1452 
1453     /**
1454      * @see org.kuali.ole.module.purap.document.service.AccountsPayableDocumentSpecificService#getPersonForCancel(org.kuali.ole.module.purap.document.AccountsPayableDocument)
1455      */
1456     @Override
1457     public Person getPersonForCancel(AccountsPayableDocument apDoc) {
1458         PaymentRequestDocument preqDoc = (PaymentRequestDocument) apDoc;
1459         Person user = null;
1460         if (preqDoc.isPaymentRequestedCancelIndicator()) {
1461             user = preqDoc.getLastActionPerformedByUser();
1462         }
1463         return user;
1464     }
1465 
1466     /**
1467      * @see org.kuali.ole.module.purap.document.service.AccountsPayableDocumentSpecificService#takePurchaseOrderCancelAction(org.kuali.ole.module.purap.document.AccountsPayableDocument)
1468      */
1469     @Override
1470     public void takePurchaseOrderCancelAction(AccountsPayableDocument apDoc) {
1471         PaymentRequestDocument preqDocument = (PaymentRequestDocument) apDoc;
1472         if (preqDocument.isReopenPurchaseOrderIndicator()) {
1473             String docType = PurapConstants.PurchaseOrderDocTypes.PURCHASE_ORDER_REOPEN_DOCUMENT;
1474             purchaseOrderService.createAndRoutePotentialChangeDocument(preqDocument.getPurchaseOrderDocument().getDocumentNumber(), docType, "reopened by Credit Memo " + apDoc.getPurapDocumentIdentifier() + "cancel", new ArrayList(), PurapConstants.PurchaseOrderStatuses.APPDOC_PENDING_REOPEN);
1475         }
1476     }
1477 
1478     /**
1479      * @see org.kuali.ole.module.purap.document.service.AccountsPayableDocumentSpecificService#updateStatusByNode(java.lang.String,
1480      *      org.kuali.ole.module.purap.document.AccountsPayableDocument)
1481      */
1482     @Override
1483     public String updateStatusByNode(String currentNodeName, AccountsPayableDocument apDoc) {
1484         return updateStatusByNode(currentNodeName, (PaymentRequestDocument) apDoc);
1485     }
1486 
1487     /**
1488      * Updates the status of the payment request document.
1489      *
1490      * @param currentNodeName The current node name.
1491      * @param preqDoc         The payment request document whose status to be updated.
1492      * @return The canceled status code.
1493      */
1494     protected String updateStatusByNode(String currentNodeName, PaymentRequestDocument preqDoc) {
1495         // remove request cancel if necessary
1496         clearRequestCancelFields(preqDoc);
1497 
1498         // update the status on the document
1499 
1500         String cancelledStatus = "";
1501         if (StringUtils.isEmpty(currentNodeName)) {
1502             // if empty probably not coming from workflow
1503             cancelledStatus = PurapConstants.PaymentRequestStatuses.APPDOC_CANCELLED_POST_AP_APPROVE;
1504         } else {
1505             cancelledStatus = PurapConstants.PaymentRequestStatuses.getPaymentRequestAppDocDisapproveStatuses().get(currentNodeName);
1506         }
1507 
1508         if (StringUtils.isNotBlank(cancelledStatus)) {
1509             try {
1510                 preqDoc.updateAndSaveAppDocStatus(cancelledStatus);
1511             } catch (WorkflowException we) {
1512                 throw new RuntimeException("Unable to save the route status data for document: " + preqDoc.getDocumentNumber(), we);
1513             }
1514             purapService.saveDocumentNoValidation(preqDoc);
1515         } else {
1516             logAndThrowRuntimeException("No status found to set for document being disapproved in node '" + currentNodeName + "'");
1517         }
1518         return cancelledStatus;
1519     }
1520 
1521     /**
1522      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#markPaid(org.kuali.ole.module.purap.document.PaymentRequestDocument,
1523      *      java.sql.Date)
1524      */
1525     @Override
1526     public void markPaid(PaymentRequestDocument pr, Date processDate) {
1527         LOG.debug("markPaid() started");
1528 
1529         pr.setPaymentPaidTimestamp(new Timestamp(processDate.getTime()));
1530         purapService.saveDocumentNoValidation(pr);
1531     }
1532 
1533     /**
1534      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#hasDiscountItem(org.kuali.ole.module.purap.document.PaymentRequestDocument)
1535      */
1536     @Override
1537     public boolean hasDiscountItem(PaymentRequestDocument preq) {
1538         return ObjectUtils.isNotNull(findDiscountItem(preq));
1539     }
1540 
1541     /**
1542      * @see org.kuali.ole.module.purap.document.service.AccountsPayableDocumentSpecificService#poItemEligibleForAp(org.kuali.ole.module.purap.document.AccountsPayableDocument,
1543      *      org.kuali.ole.module.purap.businessobject.PurchaseOrderItem)
1544      */
1545     @Override
1546     public boolean poItemEligibleForAp(AccountsPayableDocument apDoc, PurchaseOrderItem poi) {
1547         if (ObjectUtils.isNull(poi)) {
1548             throw new RuntimeException("item null in purchaseOrderItemEligibleForPayment ... this should never happen");
1549         }
1550         // if the po item is not active... skip it
1551         if (!poi.isItemActiveIndicator()) {
1552             return false;
1553         }
1554 
1555         ItemType poiType = poi.getItemType();
1556         if (ObjectUtils.isNull(poiType)) {
1557             return false;
1558         }
1559 
1560         if (poiType.isQuantityBasedGeneralLedgerIndicator()) {
1561             if (poi.getItemQuantity().isGreaterThan(poi.getItemInvoicedTotalQuantity())) {
1562                 return true;
1563             }
1564             return false;
1565         } else { // not quantity based
1566             // As long as it contains a number (whether it's 0, negative or positive number), we'll
1567             // have to return true. This is so that the OutstandingEncumberedAmount and the
1568             // Original Amount from PO column would appear on the page for Trade In.
1569             if (poi.getItemOutstandingEncumberedAmount() != null) {
1570                 return true;
1571             }
1572             return false;
1573         }
1574     }
1575 
1576     @Override
1577     public void removeIneligibleAdditionalCharges(PaymentRequestDocument document) {
1578 
1579         List<PaymentRequestItem> itemsToRemove = new ArrayList<PaymentRequestItem>();
1580 
1581         for (PaymentRequestItem item : (List<PaymentRequestItem>) document.getItems()) {
1582 
1583             // if no extended price and its an order discount or trade in, remove
1584             if (ObjectUtils.isNull(item.getPurchaseOrderItemUnitPrice()) && (ItemTypeCodes.ITEM_TYPE_ORDER_DISCOUNT_CODE.equals(item.getItemTypeCode()) || ItemTypeCodes.ITEM_TYPE_TRADE_IN_CODE.equals(item.getItemTypeCode()))) {
1585                 itemsToRemove.add(item);
1586                 continue;
1587             }
1588 
1589             // if a payment terms discount exists but not set on teh doc, remove
1590             if (StringUtils.equals(item.getItemTypeCode(), PurapConstants.ItemTypeCodes.ITEM_TYPE_PMT_TERMS_DISCOUNT_CODE)) {
1591                 PaymentTermType pt = document.getVendorPaymentTerms();
1592                 if ((pt != null) && (pt.getVendorPaymentTermsPercent() != null) && (BigDecimal.ZERO.compareTo(pt.getVendorPaymentTermsPercent()) != 0)) {
1593                     // discount ok
1594                 } else {
1595                     // remove discount
1596                     itemsToRemove.add(item);
1597                 }
1598                 continue;
1599             }
1600 
1601         }
1602 
1603         // remove items marked for removal
1604         for (PaymentRequestItem item : itemsToRemove) {
1605             document.getItems().remove(item);
1606         }
1607     }
1608 
1609     @Override
1610     public void changeVendor(PaymentRequestDocument preq, Integer headerId, Integer detailId) {
1611 
1612         VendorDetail primaryVendor = vendorService.getVendorDetail(preq.getOriginalVendorHeaderGeneratedIdentifier(), preq.getOriginalVendorDetailAssignedIdentifier());
1613 
1614         if (primaryVendor == null) {
1615             LOG.error("useAlternateVendor() primaryVendorDetail from database for header id " + headerId + " and detail id " + detailId + "is null");
1616             throw new PurError("AlternateVendor: VendorDetail from database for header id " + headerId + " and detail id " + detailId + "is null");
1617         }
1618 
1619         // set vendor detail
1620         VendorDetail vd = vendorService.getVendorDetail(headerId, detailId);
1621         if (vd == null) {
1622             LOG.error("changeVendor() VendorDetail from database for header id " + headerId + " and detail id " + detailId + "is null");
1623             throw new PurError("changeVendor: VendorDetail from database for header id " + headerId + " and detail id " + detailId + "is null");
1624         }
1625         preq.setVendorDetail(vd);
1626         preq.setVendorName(vd.getVendorName());
1627         preq.setVendorNumber(vd.getVendorNumber());
1628         preq.setVendorHeaderGeneratedIdentifier(vd.getVendorHeaderGeneratedIdentifier());
1629         preq.setVendorDetailAssignedIdentifier(vd.getVendorDetailAssignedIdentifier());
1630         preq.setVendorPaymentTermsCode(vd.getVendorPaymentTermsCode());
1631         preq.setVendorShippingPaymentTermsCode(vd.getVendorShippingPaymentTermsCode());
1632         preq.setVendorShippingTitleCode(vd.getVendorShippingTitleCode());
1633         preq.refreshReferenceObject("vendorPaymentTerms");
1634         preq.refreshReferenceObject("vendorShippingPaymentTerms");
1635 
1636         // Set vendor address
1637         String deliveryCampus = preq.getPurchaseOrderDocument().getDeliveryCampusCode();
1638         VendorAddress va = vendorService.getVendorDefaultAddress(headerId, detailId, VendorConstants.AddressTypes.REMIT, deliveryCampus);
1639         if (va == null) {
1640             va = vendorService.getVendorDefaultAddress(headerId, detailId, VendorConstants.AddressTypes.PURCHASE_ORDER, deliveryCampus);
1641         }
1642         if (va == null) {
1643             LOG.error("changeVendor() VendorAddress from database for header id " + headerId + " and detail id " + detailId + "is null");
1644             throw new PurError("changeVendor  VendorAddress from database for header id " + headerId + " and detail id " + detailId + "is null");
1645         }
1646 
1647         if (preq != null) {
1648             setVendorAddress(va, preq);
1649         } else {
1650             LOG.error("changeVendor(): Null link back to the Purchase Order.");
1651             throw new PurError("Null link back to the Purchase Order.");
1652         }
1653 
1654         // change document description
1655         preq.getDocumentHeader().setDocumentDescription(createPreqDocumentDescription(preq.getPurchaseOrderIdentifier(), preq.getVendorName()));
1656     }
1657 
1658     /**
1659      * Set the Vendor address of the given ID.
1660      *
1661      * @param addressID ID of the address to set
1662      * @param pr        PaymentRequest to set in
1663      * @return New PaymentRequest to use
1664      */
1665     protected void setVendorAddress(VendorAddress va, PaymentRequestDocument preq) {
1666 
1667         if (va != null) {
1668             preq.setVendorAddressGeneratedIdentifier(va.getVendorAddressGeneratedIdentifier());
1669             preq.setVendorAddressInternationalProvinceName(va.getVendorAddressInternationalProvinceName());
1670             preq.setVendorLine1Address(va.getVendorLine1Address());
1671             preq.setVendorLine2Address(va.getVendorLine2Address());
1672             preq.setVendorCityName(va.getVendorCityName());
1673             preq.setVendorStateCode(va.getVendorStateCode());
1674             preq.setVendorPostalCode(va.getVendorZipCode());
1675             preq.setVendorCountryCode(va.getVendorCountryCode());
1676         }
1677 
1678     }
1679 
1680     /**
1681      * Records the specified error message into the Log file and throws a runtime exception.
1682      *
1683      * @param errorMessage the error message to be logged.
1684      */
1685     protected void logAndThrowRuntimeException(String errorMessage) {
1686         this.logAndThrowRuntimeException(errorMessage, null);
1687     }
1688 
1689     /**
1690      * Records the specified error message into the Log file and throws the specified runtime exception.
1691      *
1692      * @param errorMessage the specified error message.
1693      * @param e            the specified runtime exception.
1694      */
1695     protected void logAndThrowRuntimeException(String errorMessage, Exception e) {
1696         if (ObjectUtils.isNotNull(e)) {
1697             LOG.error(errorMessage, e);
1698             throw new RuntimeException(errorMessage, e);
1699         } else {
1700             LOG.error(errorMessage);
1701             throw new RuntimeException(errorMessage);
1702         }
1703     }
1704 
1705     /**
1706      * The given document here actually needs to be a Payment Request.
1707      *
1708      * @see org.kuali.ole.module.purap.document.service.AccountsPayableDocumentSpecificService#generateGLEntriesCreateAccountsPayableDocument(org.kuali.ole.module.purap.document.AccountsPayableDocument)
1709      */
1710     @Override
1711     public void generateGLEntriesCreateAccountsPayableDocument(AccountsPayableDocument apDocument) {
1712         PaymentRequestDocument paymentRequest = (PaymentRequestDocument) apDocument;
1713         // JHK: this is not being injected because it would cause a circular reference in the Spring definitions
1714         SpringContext.getBean(PurapGeneralLedgerService.class).generateEntriesCreatePaymentRequest(paymentRequest);
1715     }
1716 
1717     /**
1718      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#hasActivePaymentRequestsForPurchaseOrder(java.lang.Integer)
1719      */
1720     @Override
1721     public boolean hasActivePaymentRequestsForPurchaseOrder(Integer purchaseOrderIdentifier) {
1722 
1723         boolean hasActivePreqs = false;
1724         List<String> docNumbers = null;
1725         WorkflowDocument workflowDocument = null;
1726 
1727         docNumbers = paymentRequestDao.getActivePaymentRequestDocumentNumbersForPurchaseOrder(purchaseOrderIdentifier);
1728         docNumbers = filterPaymentRequestByAppDocStatus(docNumbers, PaymentRequestStatuses.STATUSES_POTENTIALLY_ACTIVE);
1729 
1730         for (String docNumber : docNumbers) {
1731             try {
1732                 workflowDocument = workflowDocumentService.loadWorkflowDocument(docNumber, GlobalVariables.getUserSession().getPerson());
1733             } catch (WorkflowException we) {
1734                 throw new RuntimeException(we);
1735             }
1736             // if the document is not in a non-active status then return true and stop evaluation
1737             if (!(workflowDocument.isCanceled() || workflowDocument.isException())) {
1738                 hasActivePreqs = true;
1739                 break;
1740             }
1741         }
1742         return hasActivePreqs;
1743     }
1744 
1745     /**
1746      * Since PaymentRequest does not have the app doc status, perform an additional lookup
1747      * through doc search by using list of PaymentRequest Doc numbers.  Query appDocStatus
1748      * from workflow document and filter against the provided status
1749      * <p/>
1750      * DocumentSearch allows for multiple docNumber lookup by docId|docId|docId conversion
1751      *
1752      * @param lookupDocNumbers
1753      * @param appDocStatus
1754      * @return
1755      */
1756     protected List<String> filterPaymentRequestByAppDocStatus(List<String> lookupDocNumbers, String... appDocStatus) {
1757         boolean valid = false;
1758 
1759         final String DOC_NUM_DELIM = "|";
1760         StrBuilder routerHeaderIdBuilder = new StrBuilder().appendWithSeparators(lookupDocNumbers, DOC_NUM_DELIM);
1761 
1762         List<String> paymentRequestDocNumbers = new ArrayList<String>();
1763 
1764         DocumentSearchCriteria.Builder documentSearchCriteriaDTO = DocumentSearchCriteria.Builder.create();
1765         documentSearchCriteriaDTO.setDocumentId(routerHeaderIdBuilder.toString());
1766         documentSearchCriteriaDTO.setDocumentTypeName(PurapConstants.PurapDocTypeCodes.PAYMENT_REQUEST_DOCUMENT);
1767         documentSearchCriteriaDTO.setApplicationDocumentStatuses(Arrays.asList(appDocStatus));
1768 
1769         DocumentSearchResults reqDocumentsList = KewApiServiceLocator.getWorkflowDocumentService().documentSearch(
1770                 "", documentSearchCriteriaDTO.build());
1771 
1772         for (DocumentSearchResult reqDocument : reqDocumentsList.getSearchResults()) {
1773             ///use the appDocStatus from the KeyValueDTO result to look up custom status
1774             if (Arrays.asList(appDocStatus).contains(reqDocument.getDocument().getApplicationDocumentStatus())) {
1775                 //found the matching status, retrieve the routeHeaderId and add to the list
1776                 paymentRequestDocNumbers.add(reqDocument.getDocument().getDocumentId());
1777             }
1778 
1779         }
1780 
1781         return paymentRequestDocNumbers;
1782     }
1783 
1784     /**
1785      * Wrapper class to the filterPaymentRequestByAppDocStatus
1786      * <p/>
1787      * This class first extract the payment request document numbers from the Payment Request Collections,
1788      * then perform the filterPaymentRequestByAppDocStatus function.  Base on the filtered payment request
1789      * doc number, reconstruct the filtered Payment Request Collection
1790      *
1791      * @param paymentRequestDocuments
1792      * @param appDocStatus
1793      * @return
1794      */
1795     protected Collection<PaymentRequestDocument> filterPaymentRequestByAppDocStatus(Collection<PaymentRequestDocument> paymentRequestDocuments, String... appDocStatus) {
1796         List<String> paymentRequestDocNumbers = new ArrayList<String>();
1797         for (PaymentRequestDocument paymentRequest : paymentRequestDocuments) {
1798             paymentRequestDocNumbers.add(paymentRequest.getDocumentNumber());
1799         }
1800 
1801         List<String> filteredPaymentRequestDocNumbers = filterPaymentRequestByAppDocStatus(paymentRequestDocNumbers, appDocStatus);
1802 
1803         Collection<PaymentRequestDocument> filteredPaymentRequestDocuments = new ArrayList<PaymentRequestDocument>();
1804         //add to filtered collection if it is in the filtered payment request doc number list
1805         for (PaymentRequestDocument paymentRequest : paymentRequestDocuments) {
1806             if (filteredPaymentRequestDocNumbers.contains(paymentRequest.getDocumentNumber())) {
1807                 filteredPaymentRequestDocuments.add(paymentRequest);
1808             }
1809         }
1810         return filteredPaymentRequestDocuments;
1811     }
1812 
1813 
1814     /**
1815      * Wrapper class to the filterPaymentRequestByAppDocStatus (Collection<PaymentRequestDocument>)
1816      * <p/>
1817      * This class first construct the Payment Request Collection from the iterator, and then process through
1818      * filterPaymentRequestByAppDocStatus
1819      *
1820      * @param paymentRequestDocuments
1821      * @param appDocStatus
1822      * @return
1823      */
1824     protected Iterator<PaymentRequestDocument> filterPaymentRequestByAppDocStatus(Iterator<PaymentRequestDocument> paymentRequestIterator, String... appDocStatus) {
1825         Collection<PaymentRequestDocument> paymentRequestDocuments = new ArrayList<PaymentRequestDocument>();
1826         for (; paymentRequestIterator.hasNext(); ) {
1827             paymentRequestDocuments.add(paymentRequestIterator.next());
1828         }
1829 
1830         return filterPaymentRequestByAppDocStatus(paymentRequestDocuments, appDocStatus).iterator();
1831     }
1832 
1833     /**
1834      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#processPaymentRequestInReceivingStatus()
1835      */
1836     @Override
1837     public void processPaymentRequestInReceivingStatus() {
1838         List<String> docNumbers = paymentRequestDao.getPaymentRequestInReceivingStatus();
1839         docNumbers = filterPaymentRequestByAppDocStatus(docNumbers, PurapConstants.PaymentRequestStatuses.APPDOC_AWAITING_RECEIVING_REVIEW);
1840 
1841         List<PaymentRequestDocument> preqsAwaitingReceiving = new ArrayList<PaymentRequestDocument>();
1842         for (String docNumber : docNumbers) {
1843             PaymentRequestDocument preq = getPaymentRequestByDocumentNumber(docNumber);
1844             if (ObjectUtils.isNotNull(preq)) {
1845                 preqsAwaitingReceiving.add(preq);
1846             }
1847         }
1848         if (ObjectUtils.isNotNull(preqsAwaitingReceiving)) {
1849             for (PaymentRequestDocument preqDoc : preqsAwaitingReceiving) {
1850                 if (preqDoc.isReceivingRequirementMet()) {
1851                     try {
1852                         documentService.approveDocument(preqDoc, "Approved by Receiving Required PREQ job", null);
1853                     } catch (WorkflowException e) {
1854                         LOG.error("processPaymentRequestInReceivingStatus() Error approving payment request document from awaiting receiving", e);
1855                         throw new RuntimeException("Error approving payment request document from awaiting receiving", e);
1856                     }
1857                 }
1858             }
1859         }
1860     }
1861 
1862     /**
1863      * @see org.kuali.ole.module.purap.document.service.PaymentRequestService#allowBackpost(org.kuali.ole.module.purap.document.PaymentRequestDocument)
1864      */
1865     @Override
1866     public boolean allowBackpost(PaymentRequestDocument paymentRequestDocument) {
1867         int allowBackpost = (Integer.parseInt(parameterService.getParameterValueAsString(PaymentRequestDocument.class, PurapRuleConstants.ALLOW_BACKPOST_DAYS)));
1868 
1869         Calendar today = dateTimeService.getCurrentCalendar();
1870         Integer currentFY = universityDateService.getCurrentUniversityDate().getUniversityFiscalYear();
1871         java.util.Date priorClosingDateTemp = universityDateService.getLastDateOfFiscalYear(currentFY - 1);
1872         Calendar priorClosingDate = Calendar.getInstance();
1873         priorClosingDate.setTime(priorClosingDateTemp);
1874 
1875         // adding 1 to set the date to midnight the day after backpost is allowed so that preqs allow backpost on the last day
1876         Calendar allowBackpostDate = Calendar.getInstance();
1877         allowBackpostDate.setTime(priorClosingDate.getTime());
1878         allowBackpostDate.add(Calendar.DATE, allowBackpost + 1);
1879 
1880         Calendar preqInvoiceDate = Calendar.getInstance();
1881         preqInvoiceDate.setTime(paymentRequestDocument.getInvoiceDate());
1882 
1883         // if today is after the closing date but before/equal to the allowed backpost date and the invoice date is for the
1884         // prior year, set the year to prior year
1885         if ((today.compareTo(priorClosingDate) > 0) && (today.compareTo(allowBackpostDate) <= 0) && (preqInvoiceDate.compareTo(priorClosingDate) <= 0)) {
1886             LOG.debug("allowBackpost() within range to allow backpost; posting entry to period 12 of previous FY");
1887             return true;
1888         }
1889 
1890         LOG.debug("allowBackpost() not within range to allow backpost; posting entry to current FY");
1891         return false;
1892     }
1893 
1894     @Override
1895     public boolean isPurchaseOrderValidForPaymentRequestDocumentCreation(PaymentRequestDocument paymentRequestDocument, PurchaseOrderDocument po) {
1896         Integer POID = paymentRequestDocument.getPurchaseOrderIdentifier();
1897         boolean valid = true;
1898 
1899         PurchaseOrderDocument purchaseOrderDocument = paymentRequestDocument.getPurchaseOrderDocument();
1900         if (ObjectUtils.isNull(purchaseOrderDocument)) {
1901             GlobalVariables.getMessageMap().putError(PurapPropertyConstants.PURCHASE_ORDER_IDENTIFIER, PurapKeyConstants.ERROR_PURCHASE_ORDER_NOT_EXIST);
1902             valid &= false;
1903         } else if (purchaseOrderDocument.isPendingActionIndicator()) {
1904             GlobalVariables.getMessageMap().putError(PurapPropertyConstants.PURCHASE_ORDER_IDENTIFIER, PurapKeyConstants.ERROR_PURCHASE_PENDING_ACTION);
1905             valid &= false;
1906         } else if (!StringUtils.equals(purchaseOrderDocument.getApplicationDocumentStatus(), PurapConstants.PurchaseOrderStatuses.APPDOC_OPEN)) {
1907             GlobalVariables.getMessageMap().putError(PurapPropertyConstants.PURCHASE_ORDER_IDENTIFIER, PurapKeyConstants.ERROR_PURCHASE_ORDER_NOT_OPEN);
1908             valid &= false;
1909             // if the PO is pending and it is not a Retransmit, we cannot generate a Payment Request for it
1910         } else {
1911             // Verify that there exists at least 1 item left to be invoiced
1912             // valid &= encumberedItemExistsForInvoicing(purchaseOrderDocument);
1913         }
1914 
1915         return valid;
1916     }
1917 
1918     @Override
1919     public boolean encumberedItemExistsForInvoicing(PurchaseOrderDocument document) {
1920         boolean zeroDollar = true;
1921         GlobalVariables.getMessageMap().clearErrorPath();
1922         GlobalVariables.getMessageMap().addToErrorPath(OLEPropertyConstants.DOCUMENT);
1923         for (PurchaseOrderItem poi : (List<PurchaseOrderItem>) document.getItems()) {
1924             // Quantity-based items
1925             if (poi.getItemType().isLineItemIndicator() && poi.getItemType().isQuantityBasedGeneralLedgerIndicator()) {
1926                 KualiDecimal encumberedQuantity = poi.getItemOutstandingEncumberedQuantity() == null ? KualiDecimal.ZERO : poi.getItemOutstandingEncumberedQuantity();
1927                 if (encumberedQuantity.compareTo(KualiDecimal.ZERO) == 1) {
1928                     zeroDollar = false;
1929                     break;
1930                 }
1931             }
1932             // Service Items or Below-the-line Items
1933             else if (poi.getItemType().isAmountBasedGeneralLedgerIndicator() || poi.getItemType().isAdditionalChargeIndicator()) {
1934                 KualiDecimal encumberedAmount = poi.getItemOutstandingEncumberedAmount() == null ? KualiDecimal.ZERO : poi.getItemOutstandingEncumberedAmount();
1935                 if (encumberedAmount.compareTo(KualiDecimal.ZERO) == 1) {
1936                     zeroDollar = false;
1937                     break;
1938                 }
1939             }
1940         }
1941 
1942         return !zeroDollar;
1943     }
1944 
1945     public void setDateTimeService(DateTimeService dateTimeService) {
1946         this.dateTimeService = dateTimeService;
1947     }
1948 
1949     public void setParameterService(ParameterService parameterService) {
1950         this.parameterService = parameterService;
1951     }
1952 
1953     public void setConfigurationService(ConfigurationService configurationService) {
1954         this.configurationService = configurationService;
1955     }
1956 
1957     public void setDocumentService(DocumentService documentService) {
1958         this.documentService = documentService;
1959     }
1960 
1961     public void setNoteService(NoteService noteService) {
1962         this.noteService = noteService;
1963     }
1964 
1965     public void setPurapService(PurapService purapService) {
1966         this.purapService = purapService;
1967     }
1968 
1969     public void setPaymentRequestDao(PaymentRequestDao paymentRequestDao) {
1970         this.paymentRequestDao = paymentRequestDao;
1971     }
1972 
1973     public void setNegativePaymentRequestApprovalLimitService(NegativePaymentRequestApprovalLimitService negativePaymentRequestApprovalLimitService) {
1974         this.negativePaymentRequestApprovalLimitService = negativePaymentRequestApprovalLimitService;
1975     }
1976 
1977     public void setPurapAccountingService(PurapAccountingService purapAccountingService) {
1978         this.purapAccountingService = purapAccountingService;
1979     }
1980 
1981     public void setBusinessObjectService(BusinessObjectService businessObjectService) {
1982         this.businessObjectService = businessObjectService;
1983     }
1984 
1985     public void setPurapWorkflowIntegrationService(PurApWorkflowIntegrationService purapWorkflowIntegrationService) {
1986         this.purapWorkflowIntegrationService = purapWorkflowIntegrationService;
1987     }
1988 
1989     public void setWorkflowDocumentService(WorkflowDocumentService workflowDocumentService) {
1990         this.workflowDocumentService = workflowDocumentService;
1991     }
1992 
1993     public void setAccountsPayableService(AccountsPayableService accountsPayableService) {
1994         this.accountsPayableService = accountsPayableService;
1995     }
1996 
1997     public void setVendorService(VendorService vendorService) {
1998         this.vendorService = vendorService;
1999     }
2000 
2001     public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
2002         this.dataDictionaryService = dataDictionaryService;
2003     }
2004 
2005     public void setUniversityDateService(UniversityDateService universityDateService) {
2006         this.universityDateService = universityDateService;
2007     }
2008 
2009 
2010     public void setBankService(BankService bankService) {
2011         this.bankService = bankService;
2012     }
2013 
2014 
2015     public void setPurchaseOrderService(PurchaseOrderService purchaseOrderService) {
2016         this.purchaseOrderService = purchaseOrderService;
2017     }
2018 
2019     public void setFinancialSystemWorkflowHelperService(FinancialSystemWorkflowHelperService financialSystemWorkflowHelperService) {
2020         this.financialSystemWorkflowHelperService = financialSystemWorkflowHelperService;
2021     }
2022 
2023 
2024     public void setKualiRuleService(KualiRuleService kualiRuleService) {
2025         this.kualiRuleService = kualiRuleService;
2026     }
2027 
2028     /**
2029      * Gets the accountsPayableService attribute.
2030      *
2031      * @return Returns the accountsPayableService
2032      */
2033 
2034     public AccountsPayableService getAccountsPayableService() {
2035         return SpringContext.getBean(AccountsPayableService.class);
2036     }
2037 }