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