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