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