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