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