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