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.service.impl; 017 018 import java.math.BigDecimal; 019 import java.util.Collection; 020 import java.util.HashMap; 021 import java.util.HashSet; 022 import java.util.List; 023 import java.util.Map; 024 import java.util.Set; 025 026 import org.apache.commons.lang.ArrayUtils; 027 import org.apache.commons.lang.StringUtils; 028 import org.apache.commons.lang.math.NumberUtils; 029 import org.apache.log4j.Logger; 030 import org.kuali.kfs.module.purap.PurapConstants; 031 import org.kuali.kfs.module.purap.PurapKeyConstants; 032 import org.kuali.kfs.module.purap.PurapParameterConstants; 033 import org.kuali.kfs.module.purap.batch.ElectronicInvoiceStep; 034 import org.kuali.kfs.module.purap.businessobject.ElectronicInvoice; 035 import org.kuali.kfs.module.purap.businessobject.ElectronicInvoiceDetailRequestSummary; 036 import org.kuali.kfs.module.purap.businessobject.ElectronicInvoiceRejectReason; 037 import org.kuali.kfs.module.purap.businessobject.ElectronicInvoiceRejectReasonType; 038 import org.kuali.kfs.module.purap.businessobject.PurApItem; 039 import org.kuali.kfs.module.purap.businessobject.PurchaseOrderItem; 040 import org.kuali.kfs.module.purap.document.PurchaseOrderDocument; 041 import org.kuali.kfs.module.purap.service.ElectronicInvoiceMatchingService; 042 import org.kuali.kfs.module.purap.util.ElectronicInvoiceUtils; 043 import org.kuali.kfs.module.purap.util.PurApItemUtils; 044 import org.kuali.kfs.sys.context.SpringContext; 045 import org.kuali.kfs.sys.service.TaxService; 046 import org.kuali.kfs.sys.service.impl.KfsParameterConstants; 047 import org.kuali.kfs.vnd.businessobject.PurchaseOrderCostSource; 048 import org.kuali.kfs.vnd.businessobject.VendorDetail; 049 import org.kuali.kfs.vnd.document.service.VendorService; 050 import org.kuali.rice.kns.service.BusinessObjectService; 051 import org.kuali.rice.kns.service.DateTimeService; 052 import org.kuali.rice.kns.service.ParameterService; 053 import org.kuali.rice.kns.util.KualiDecimal; 054 import org.kuali.rice.kns.util.ObjectUtils; 055 056 public class ElectronicInvoiceMatchingServiceImpl implements ElectronicInvoiceMatchingService { 057 058 private Logger LOG = Logger.getLogger(ElectronicInvoiceMatchingServiceImpl.class); 059 060 private Map<String,ElectronicInvoiceRejectReasonType> rejectReasonTypes; 061 private VendorService vendorService; 062 private TaxService taxService; 063 private DateTimeService dateTimeService; 064 065 String upperVariancePercentString; 066 String lowerVariancePercentString; 067 068 public void doMatchingProcess(ElectronicInvoiceOrderHolder orderHolder) { 069 070 if (LOG.isInfoEnabled()){ 071 LOG.info("Matching process started"); 072 } 073 074 upperVariancePercentString = SpringContext.getBean(ParameterService.class).getParameterValue(ElectronicInvoiceStep.class, PurapParameterConstants.ElectronicInvoiceParameters.SALES_TAX_UPPER_VARIANCE_PERCENT); 075 lowerVariancePercentString = SpringContext.getBean(ParameterService.class).getParameterValue(ElectronicInvoiceStep.class, PurapParameterConstants.ElectronicInvoiceParameters.SALES_TAX_LOWER_VARIANCE_PERCENT);; 076 077 try { 078 if (orderHolder.isValidateHeaderInformation()) { 079 080 validateHeaderInformation(orderHolder); 081 082 if (orderHolder.isInvoiceRejected()) { 083 if (LOG.isInfoEnabled()){ 084 LOG.info("Matching process failed at header validation"); 085 } 086 return; 087 } 088 } 089 090 validateInvoiceDetails(orderHolder); 091 092 if (orderHolder.isInvoiceRejected()) { 093 if (LOG.isInfoEnabled()){ 094 LOG.info("Matching process failed at order detail validation"); 095 } 096 return; 097 } 098 099 } 100 catch (NumberFormatException e) { 101 if (LOG.isInfoEnabled()){ 102 LOG.info("Matching process matching failed due to number format exception " + e.getMessage()); 103 } 104 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVALID_NUMBER_FORMAT, e.getMessage(), orderHolder.getFileName()); 105 orderHolder.addInvoiceHeaderRejectReason(rejectReason); 106 return; 107 } 108 109 if (LOG.isInfoEnabled()){ 110 LOG.info("Matching process ended successfully"); 111 } 112 } 113 114 protected void validateHeaderInformation(ElectronicInvoiceOrderHolder orderHolder){ 115 116 String dunsField = PurapConstants.ElectronicInvoice.RejectDocumentFields.VENDOR_DUNS_NUMBER; 117 String applnResourceKeyName = PurapKeyConstants.ERROR_REJECT_INVALID_DUNS; 118 119 if (StringUtils.isEmpty(orderHolder.getDunsNumber())){ 120 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.DUNS_NOT_FOUND,null,orderHolder.getFileName()); 121 orderHolder.addInvoiceHeaderRejectReason(rejectReason,dunsField,applnResourceKeyName); 122 return; 123 } 124 125 if (orderHolder.isRejectDocumentHolder()){ 126 VendorDetail vendorDetail = SpringContext.getBean(VendorService.class).getVendorByDunsNumber(orderHolder.getDunsNumber()); 127 if (vendorDetail == null){ 128 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.DUNS_INVALID, null, orderHolder.getFileName()); 129 orderHolder.addInvoiceHeaderRejectReason(rejectReason,dunsField,applnResourceKeyName); 130 return; 131 } 132 }else{ 133 if (orderHolder.getVendorHeaderId() == null && orderHolder.getVendorDetailId() == null) { 134 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.DUNS_INVALID, null, orderHolder.getFileName()); 135 orderHolder.addInvoiceHeaderRejectReason(rejectReason,dunsField,applnResourceKeyName); 136 return; 137 } 138 } 139 140 String invoiceNumberField = PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_FILE_NUMBER; 141 if (!orderHolder.isInvoiceNumberAcceptIndicatorEnabled()){ 142 if (StringUtils.isEmpty(orderHolder.getInvoiceNumber())){ 143 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_ID_EMPTY,null,orderHolder.getFileName()); 144 orderHolder.addInvoiceHeaderRejectReason(rejectReason,invoiceNumberField,PurapKeyConstants.ERROR_REJECT_INVOICE_NUMBER_EMPTY); 145 return; 146 } 147 } 148 149 String invoiceDateField = PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_FILE_DATE; 150 151 if (StringUtils.isEmpty(orderHolder.getInvoiceDateString()) || orderHolder.getInvoiceDate() == null){ 152 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_DATE_INVALID,null,orderHolder.getFileName()); 153 orderHolder.addInvoiceHeaderRejectReason(rejectReason,invoiceDateField,PurapKeyConstants.ERROR_REJECT_INVOICE_DATE_INVALID); 154 return; 155 }else if (orderHolder.getInvoiceDate().after(dateTimeService.getCurrentDate())) { 156 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_DATE_GREATER,null,orderHolder.getFileName()); 157 orderHolder.addInvoiceOrderRejectReason(rejectReason,invoiceDateField,PurapKeyConstants.ERROR_REJECT_INVOICE_DATE_GREATER); 158 return; 159 } 160 161 if (orderHolder.isInformationOnly()){ 162 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INFORMATION_ONLY,null,orderHolder.getFileName()); 163 orderHolder.addInvoiceHeaderRejectReason(rejectReason); 164 return; 165 } 166 167 validateSummaryAmounts(orderHolder); 168 169 if (orderHolder.isInvoiceRejected()) { 170 return; 171 } 172 173 validateItemTypes(orderHolder); 174 175 if (orderHolder.isInvoiceRejected()) { 176 return; 177 } 178 179 } 180 181 protected void validateSummaryAmounts(ElectronicInvoiceOrderHolder orderHolder) { 182 183 if (orderHolder.isRejectDocumentHolder()){ 184 /** 185 * If there are any rejects related to the summary, we're retaining it since 186 * it's not possible to get the summary amount totals from the reject doc 187 */ 188 return; 189 } 190 191 ElectronicInvoiceDetailRequestSummary summary = orderHolder.getElectronicInvoice().getInvoiceDetailRequestSummary(); 192 193 boolean enableSalesTaxInd = SpringContext.getBean(ParameterService.class).getIndicatorParameter(KfsParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.ENABLE_SALES_TAX_IND); 194 195 boolean salesTaxUsed = false; 196 PurchaseOrderDocument poDoc = orderHolder.getPurchaseOrderDocument(); 197 if (poDoc != null) { // we handle bad PO's in the eInvoice later, so just skip this 198 List<PurApItem> items = PurApItemUtils.getAboveTheLineOnly(poDoc.getItems()); 199 for (PurApItem item : items) { 200 if (item.getItemType().isTaxableIndicator()) { 201 salesTaxUsed = true; 202 break; 203 } 204 } 205 206 boolean useTaxUsed = poDoc.isUseTaxIndicator(); 207 enableSalesTaxInd &= (salesTaxUsed || useTaxUsed); 208 209 BigDecimal summaryTaxAmount = summary.getInvoiceTaxAmount(); 210 if (!enableSalesTaxInd) { 211 // if sales tax is disabled, total tax amount shall be zero 212 if (summaryTaxAmount.compareTo(new BigDecimal(0)) != 0) { 213 String extraDescription = "Summary Tax Amount:" + summaryTaxAmount; 214 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.TAX_SUMMARY_AMT_EXISTS, extraDescription, orderHolder.getFileName()); 215 orderHolder.addInvoiceHeaderRejectReason(rejectReason); 216 } 217 } 218 else if (orderHolder.isTaxInLine()) { 219 validateSummaryAmount(orderHolder, summaryTaxAmount, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_TAX, PurapConstants.ElectronicInvoice.TAX_SUMMARY_AMT_MISMATCH); 220 } 221 } 222 223 if (orderHolder.isShippingInLine()) { 224 validateSummaryAmount(orderHolder, summary.getInvoiceShippingAmount(), ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_SHIPPING, PurapConstants.ElectronicInvoice.SHIPPING_SUMMARY_AMT_MISMATCH); 225 } 226 227 if (orderHolder.isSpecialHandlingInLine()) { 228 validateSummaryAmount(orderHolder, summary.getInvoiceSpecialHandlingAmount(), ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_SPECIAL_HANDLING, PurapConstants.ElectronicInvoice.SPL_HANDLING_SUMMARY_AMT_MISMATCH); 229 } 230 231 if (orderHolder.isDiscountInLine()) { 232 validateSummaryAmount(orderHolder, summary.getInvoiceDiscountAmount(), ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_DISCOUNT, PurapConstants.ElectronicInvoice.DISCOUNT_SUMMARY_AMT_MISMATCH); 233 } 234 235 } 236 237 protected void validateSummaryAmount(ElectronicInvoiceOrderHolder orderHolder, 238 BigDecimal summaryAmount, 239 String invoiceLineItemTypeCode, 240 String rejectDescriptionCode) { 241 242 BigDecimal lineItemTotalAmount = orderHolder.getElectronicInvoice().getFileTotalAmountForInLineItems(invoiceLineItemTypeCode); 243 244 // if (lineItemTotalAmount.compareTo(BigDecimal.ZERO) != 0) { // old way, but it's not needed 245 if ((lineItemTotalAmount.compareTo(summaryAmount)) != 0) { 246 String extraDescription = "Line Total Amount:" + lineItemTotalAmount + ",Summary Total Amount:" + summaryAmount; 247 ElectronicInvoiceRejectReason rejectReason = createRejectReason(rejectDescriptionCode, extraDescription, orderHolder.getFileName()); 248 orderHolder.addInvoiceHeaderRejectReason(rejectReason); 249 } 250 // } 251 } 252 253 protected void validateItemTypes(ElectronicInvoiceOrderHolder orderHolder) { 254 255 validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_ITEM); 256 validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_TAX); 257 validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_SHIPPING); 258 validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_SPECIAL_HANDLING); 259 validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_DISCOUNT); 260 validateItemMapping(orderHolder, ElectronicInvoice.INVOICE_AMOUNT_TYPE_CODE_EXMT); 261 262 } 263 264 protected void validateItemMapping(ElectronicInvoiceOrderHolder orderHolder, String kualiItemTypeCode) { 265 266 if (!orderHolder.isItemTypeAvailableInItemMapping(kualiItemTypeCode)) { 267 String extraDescription = kualiItemTypeCode; 268 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.ITEM_MAPPING_NOT_AVAILABLE, extraDescription, orderHolder.getFileName()); 269 orderHolder.addInvoiceHeaderRejectReason(rejectReason); 270 return; 271 } 272 273 } 274 275 protected void validateInvoiceDetails(ElectronicInvoiceOrderHolder orderHolder){ 276 277 validatePurchaseOrderMatch(orderHolder); 278 279 if (orderHolder.isInvoiceRejected()){ 280 return; 281 } 282 283 validateInvoiceItems(orderHolder); 284 285 if (LOG.isInfoEnabled()){ 286 if (!orderHolder.isInvoiceRejected()){ 287 LOG.info("Purchase order document match done successfully"); 288 } 289 } 290 } 291 292 protected void validatePurchaseOrderMatch(ElectronicInvoiceOrderHolder orderHolder){ 293 294 String poIDFieldName = PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_PO_ID; 295 String poID = orderHolder.getInvoicePurchaseOrderID(); 296 297 if (StringUtils.isEmpty(poID)){ 298 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_ID_EMPTY,null,orderHolder.getFileName()); 299 orderHolder.addInvoiceOrderRejectReason(rejectReason,poIDFieldName,PurapKeyConstants.ERROR_REJECT_INVOICE_POID_EMPTY); 300 return; 301 } 302 303 String extraDesc = "Invoice Order ID:" + poID; 304 305 if (!NumberUtils.isDigits(poID)){ 306 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_ID_INVALID_FORMAT,extraDesc,orderHolder.getFileName()); 307 orderHolder.addInvoiceOrderRejectReason(rejectReason,poIDFieldName,PurapKeyConstants.ERROR_REJECT_INVOICE_POID_INVALID); 308 return; 309 } 310 311 PurchaseOrderDocument poDoc = orderHolder.getPurchaseOrderDocument(); 312 313 if (poDoc == null){ 314 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_NOT_EXISTS,extraDesc,orderHolder.getFileName()); 315 orderHolder.addInvoiceOrderRejectReason(rejectReason,poIDFieldName,PurapKeyConstants.ERROR_REJECT_INVOICE__PO_NOT_EXISTS); 316 return; 317 } 318 319 if (poDoc.getVendorHeaderGeneratedIdentifier() == null || 320 poDoc.getVendorDetailAssignedIdentifier() == null || 321 !(poDoc.getVendorHeaderGeneratedIdentifier().equals(orderHolder.getVendorHeaderId()) && 322 poDoc.getVendorDetailAssignedIdentifier().equals(orderHolder.getVendorDetailId()))){ 323 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_VENDOR_NOT_MATCHES_WITH_INVOICE_VENDOR,null,orderHolder.getFileName()); 324 orderHolder.addInvoiceOrderRejectReason(rejectReason); 325 return; 326 } 327 328 } 329 330 protected void validateInvoiceItems(ElectronicInvoiceOrderHolder orderHolder){ 331 332 Set poLineNumbers = new HashSet(); 333 334 ElectronicInvoiceItemHolder[] itemHolders = orderHolder.getItems(); 335 if (itemHolders != null){ 336 for (int i = 0; i < itemHolders.length; i++) { 337 validateInvoiceItem(itemHolders[i],poLineNumbers); 338 } 339 } 340 } 341 342 protected void validateInvoiceItem(ElectronicInvoiceItemHolder itemHolder, 343 Set poLineNumbers){ 344 345 PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem(); 346 ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder(); 347 348 if (poItem == null){ 349 String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber(); 350 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.NO_MATCHING_PO_ITEM,extraDescription,orderHolder.getFileName()); 351 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER,PurapKeyConstants.ERROR_REJECT_INVOICE__ITEM_NOMATCH); 352 return; 353 } 354 355 if (poLineNumbers.contains(itemHolder.getInvoiceItemLineNumber())){ 356 String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber(); 357 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.DUPLIATE_INVOICE_LINE_ITEM,extraDescription,orderHolder.getFileName()); 358 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER,PurapKeyConstants.ERROR_REJECT_PO_ITEM_DUPLICATE); 359 return; 360 }else{ 361 poLineNumbers.add(itemHolder.getInvoiceItemLineNumber()); 362 } 363 364 if (!poItem.isItemActiveIndicator()){ 365 String extraDescription = "PO Item Line Number:" + poItem.getItemLineNumber(); 366 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INACTIVE_LINE_ITEM,extraDescription,orderHolder.getFileName()); 367 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER,PurapKeyConstants.ERROR_REJECT_PO_ITEM_INACTIVE); 368 return; 369 } 370 371 if (!itemHolder.isCatalogNumberAcceptIndicatorEnabled()){ 372 validateCatalogNumber(itemHolder); 373 if (orderHolder.isInvoiceRejected()){ 374 return; 375 } 376 } 377 378 if (!itemHolder.isUnitOfMeasureAcceptIndicatorEnabled()){ 379 if (!StringUtils.equals(poItem.getItemUnitOfMeasureCode(), itemHolder.getInvoiceItemUnitOfMeasureCode())){ 380 String extraDescription = "Invoice UOM:" + itemHolder.getInvoiceItemUnitOfMeasureCode() + ", PO UOM:" + poItem.getItemUnitOfMeasureCode(); 381 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.UNIT_OF_MEASURE_MISMATCH,extraDescription,orderHolder.getFileName()); 382 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_UOM,PurapKeyConstants.ERROR_REJECT_UOM_MISMATCH); 383 return; 384 } 385 } 386 387 validateUnitPrice(itemHolder); 388 389 if (orderHolder.isInvoiceRejected()){ 390 return; 391 } 392 393 validateSalesTax(itemHolder); 394 395 if (orderHolder.isInvoiceRejected()){ 396 return; 397 } 398 399 if (poItem.getItemQuantity() != null) { 400 validateQtyBasedItem(itemHolder); 401 }else{ 402 validateNonQtyBasedItem(itemHolder); 403 } 404 405 } 406 407 protected void validateCatalogNumber(ElectronicInvoiceItemHolder itemHolder){ 408 409 PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem(); 410 ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder(); 411 412 String invoiceCatalogNumberStripped = itemHolder.getCatalogNumberStripped(); 413 String poCatalogNumberStripped = ElectronicInvoiceUtils.stripSplChars(poItem.getItemCatalogNumber()); 414 415 /** 416 * If Catalog number in invoice and po are not empty, create reject reason if it doesn't match 417 */ 418 if (StringUtils.isNotBlank(invoiceCatalogNumberStripped) && 419 StringUtils.isNotBlank(poCatalogNumberStripped)){ 420 421 if (!StringUtils.equals(poCatalogNumberStripped, invoiceCatalogNumberStripped)){ 422 423 String extraDescription = "Invoice Catalog No:" + invoiceCatalogNumberStripped + ", PO Catalog No:" + poCatalogNumberStripped; 424 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.CATALOG_NUMBER_MISMATCH,extraDescription,orderHolder.getFileName()); 425 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_CATALOG_NUMBER,PurapKeyConstants.ERROR_REJECT_CATALOG_MISMATCH); 426 } 427 428 }else{ 429 430 /** 431 * If catalog number is empty in PO/&Invoice, check whether the catalog check is required for the requisition source. 432 * If exists in param, create reject reason. 433 * If not exists, continue with UOM and unit price match. 434 */ 435 String reqSourceRequiringCatalogMatch = SpringContext.getBean(ParameterService.class).getParameterValue(ElectronicInvoiceStep.class, PurapParameterConstants.ElectronicInvoiceParameters.REQUISITION_SOURCES_REQUIRING_CATALOG_MATCHING); 436 String requisitionSourceCodeInPO = orderHolder.getPurchaseOrderDocument().getRequisitionSourceCode(); 437 438 if (StringUtils.isNotEmpty(reqSourceRequiringCatalogMatch)){ 439 String[] requisitionSourcesFromParam = StringUtils.split(reqSourceRequiringCatalogMatch,';'); 440 if (ArrayUtils.contains(requisitionSourcesFromParam, requisitionSourceCodeInPO)){ 441 String extraDescription = "Invoice Catalog No:" + invoiceCatalogNumberStripped + ", PO Catalog No:" + poItem.getItemCatalogNumber(); 442 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.CATALOG_NUMBER_MISMATCH,extraDescription,orderHolder.getFileName()); 443 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_CATALOG_NUMBER,PurapKeyConstants.ERROR_REJECT_CATALOG_MISMATCH); 444 } 445 } 446 } 447 } 448 449 protected void validateQtyBasedItem(ElectronicInvoiceItemHolder itemHolder){ 450 451 PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem(); 452 453 String fileName = itemHolder.getInvoiceOrderHolder().getFileName(); 454 ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder(); 455 456 if (KualiDecimal.ZERO.compareTo(poItem.getItemOutstandingEncumberedQuantity()) >= 0) { 457 //we have no quantity left encumbered on the po item 458 String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber(); 459 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.OUTSTANDING_ENCUMBERED_QTY_AVAILABLE,extraDescription,orderHolder.getFileName()); 460 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_QUANTITY,PurapKeyConstants.ERROR_REJECT_POITEM_OUTSTANDING_QTY); 461 return; 462 } 463 464 if (itemHolder.getInvoiceItemQuantity() == null){ 465 //we have quantity entered on the PO Item but the Invoice has no quantity 466 String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber(); 467 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_QTY_EMPTY,extraDescription,orderHolder.getFileName()); 468 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_QUANTITY,PurapKeyConstants.ERROR_REJECT_POITEM_INVOICE_QTY_EMPTY); 469 return; 470 }else{ 471 472 if(!itemHolder.getInvoiceOrderHolder().getPurchaseOrderDocument().isReceivingDocumentRequiredIndicator()){ 473 474 if ((itemHolder.getInvoiceItemQuantity().compareTo(poItem.getItemOutstandingEncumberedQuantity().bigDecimalValue())) > 0) { 475 //we have more quantity on the e-invoice than left outstanding encumbered on the PO item 476 String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber(); 477 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_ITEM_QTY_LESSTHAN_INVOICE_ITEM_QTY,extraDescription,orderHolder.getFileName()); 478 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_QUANTITY,PurapKeyConstants.ERROR_REJECT_POITEM_LESS_OUTSTANDING_QTY); 479 return; 480 } 481 } 482 } 483 484 } 485 486 protected void validateNonQtyBasedItem(ElectronicInvoiceItemHolder itemHolder){ 487 488 PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem(); 489 490 String fileName = itemHolder.getInvoiceOrderHolder().getFileName(); 491 ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder(); 492 493 if ((KualiDecimal.ZERO.compareTo(poItem.getItemOutstandingEncumberedAmount())) >= 0) { 494 //we have no dollars left encumbered on the po item 495 String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber(); 496 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.OUTSTANDING_ENCUMBERED_AMT_AVAILABLE,extraDescription,orderHolder.getFileName()); 497 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER,PurapKeyConstants.ERROR_REJECT_POITEM_OUTSTANDING_EMCUMBERED_AMOUNT); 498 return; 499 }else{ 500 //we have encumbered dollars left on PO 501 if (((itemHolder.getInvoiceItemSubTotalAmount().setScale(KualiDecimal.SCALE, KualiDecimal.ROUND_BEHAVIOR)).compareTo(poItem.getItemOutstandingEncumberedAmount().bigDecimalValue())) > 0) { 502 String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber(); 503 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.PO_ITEM_AMT_LESSTHAN_INVOICE_ITEM_AMT,extraDescription,orderHolder.getFileName()); 504 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_LINE_NUMBER,PurapKeyConstants.ERROR_REJECT_POITEM_LESS_OUTSTANDING_EMCUMBERED_AMOUNT); 505 return; 506 } 507 508 } 509 } 510 511 protected void validateUnitPrice(ElectronicInvoiceItemHolder itemHolder){ 512 513 PurchaseOrderCostSource costSource = itemHolder.getInvoiceOrderHolder().getPurchaseOrderDocument().getPurchaseOrderCostSource(); 514 PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem(); 515 ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder(); 516 517 String extraDescription = "Invoice Item Line Number:" + itemHolder.getInvoiceItemLineNumber(); 518 519 BigDecimal actualVariance = itemHolder.getInvoiceItemUnitPrice().subtract(poItem.getItemUnitPrice()); 520 521 BigDecimal lowerPercentage = null; 522 if (costSource.getItemUnitPriceLowerVariancePercent() != null){ 523 //Checking for lower variance 524 lowerPercentage = costSource.getItemUnitPriceLowerVariancePercent(); 525 } 526 else { 527 //If the cost source itemUnitPriceLowerVariancePercent is null then 528 //we'll use the exact match (100%). 529 lowerPercentage = new BigDecimal(100); 530 } 531 532 BigDecimal lowerAcceptableVariance = (lowerPercentage.divide(new BigDecimal(100))).multiply(poItem.getItemUnitPrice()).negate(); 533 534 if (lowerAcceptableVariance.compareTo(actualVariance) > 0) { 535 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_AMT_LESSER_THAN_LOWER_VARIANCE, extraDescription, orderHolder.getFileName()); 536 orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_UNIT_PRICE, PurapKeyConstants.ERROR_REJECT_UNITPRICE_LOWERVARIANCE); 537 } 538 539 BigDecimal upperPercentage = null; 540 541 if (costSource.getItemUnitPriceUpperVariancePercent() != null){ 542 //Checking for upper variance 543 upperPercentage = costSource.getItemUnitPriceUpperVariancePercent(); 544 } 545 else { 546 //If the cost source itemUnitPriceLowerVariancePercent is null then 547 //we'll use the exact match (100%). 548 upperPercentage = new BigDecimal(100); 549 } 550 BigDecimal upperAcceptableVariance = (upperPercentage.divide(new BigDecimal(100))).multiply(poItem.getItemUnitPrice()); 551 552 if (upperAcceptableVariance.compareTo(actualVariance) < 0) { 553 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.INVOICE_AMT_GREATER_THAN_UPPER_VARIANCE, extraDescription, orderHolder.getFileName()); 554 orderHolder.addInvoiceOrderRejectReason(rejectReason, PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_UNIT_PRICE, PurapKeyConstants.ERROR_REJECT_UNITPRICE_UPPERVARIANCE); 555 } 556 557 } 558 559 protected void validateSalesTax(ElectronicInvoiceItemHolder itemHolder){ 560 561 if (LOG.isInfoEnabled()){ 562 LOG.info("Validating sales tax"); 563 } 564 565 ElectronicInvoiceOrderHolder orderHolder = itemHolder.getInvoiceOrderHolder(); 566 PurchaseOrderItem poItem = itemHolder.getPurchaseOrderItem(); 567 KualiDecimal invoiceSalesTaxAmount = new KualiDecimal(itemHolder.getTaxAmount()); 568 569 boolean enableSalesTaxInd = SpringContext.getBean(ParameterService.class).getIndicatorParameter(KfsParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.ENABLE_SALES_TAX_IND); 570 571 boolean salesTaxUsed = false; 572 PurchaseOrderDocument poDoc = orderHolder.getPurchaseOrderDocument(); 573 List<PurApItem> items = PurApItemUtils.getAboveTheLineOnly(poDoc.getItems()); 574 for (PurApItem item : items) { 575 if (item.getItemType().isTaxableIndicator()) { 576 salesTaxUsed = true; 577 break; 578 } 579 } 580 boolean useTaxUsed = poDoc.isUseTaxIndicator(); 581 enableSalesTaxInd &= (poItem.getItemType().isTaxableIndicator() && (salesTaxUsed || useTaxUsed)); 582 583 if (LOG.isInfoEnabled()){ 584 LOG.info("Sales Tax Enable Indicator - " + enableSalesTaxInd); 585 LOG.info("Invoice item tax amount - " + invoiceSalesTaxAmount); 586 } 587 if (!enableSalesTaxInd) { 588 // if sales tax is disabled, item tax amount shall be zero 589 if (invoiceSalesTaxAmount.compareTo(KualiDecimal.ZERO) != 0) { 590 String extraDescription = "Item Tax Amount:" + invoiceSalesTaxAmount; 591 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.TAX_SUMMARY_AMT_EXISTS, extraDescription, orderHolder.getFileName()); 592 orderHolder.addInvoiceHeaderRejectReason(rejectReason); 593 } 594 return; 595 } 596 597 // For reject doc, trans date should be the einvoice processed date. 598 java.sql.Date transTaxDate = itemHolder.getInvoiceOrderHolder().getInvoiceProcessedDate(); 599 String deliveryPostalCode = poItem.getPurchaseOrder().getDeliveryPostalCode(); 600 KualiDecimal extendedPrice = new KualiDecimal(getExtendedPrice(itemHolder).setScale(KualiDecimal.SCALE, KualiDecimal.ROUND_BEHAVIOR)); 601 602 KualiDecimal salesTaxAmountCalculated = taxService.getTotalSalesTaxAmount(transTaxDate, deliveryPostalCode, extendedPrice); 603 KualiDecimal actualVariance = invoiceSalesTaxAmount.subtract(salesTaxAmountCalculated); 604 605 if (LOG.isInfoEnabled()){ 606 LOG.info("Sales Tax Upper Variance param - " + upperVariancePercentString); 607 LOG.info("Sales Tax Lower Variance param - " + lowerVariancePercentString); 608 LOG.info("Trans date (from invoice/rejectdoc) - " + transTaxDate); 609 LOG.info("Delivery Postal Code - " + deliveryPostalCode); 610 LOG.info("Extended price - " + extendedPrice); 611 LOG.info("Sales Tax amount (from sales tax service) - " + salesTaxAmountCalculated); 612 } 613 614 if (StringUtils.isNotEmpty(upperVariancePercentString)){ 615 616 KualiDecimal upperVariancePercent = new KualiDecimal(upperVariancePercentString); 617 BigDecimal upperAcceptableVariance = (upperVariancePercent.divide(new KualiDecimal(100))).multiply(salesTaxAmountCalculated).bigDecimalValue(); 618 619 if (upperAcceptableVariance.compareTo(actualVariance.bigDecimalValue()) < 0){ 620 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.SALES_TAX_AMT_GREATER_THAN_UPPER_VARIANCE,null,orderHolder.getFileName()); 621 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_TAX_AMT,PurapKeyConstants.ERROR_REJECT_TAXAMOUNT_UPPERVARIANCE); 622 return; 623 } 624 625 } 626 627 if (StringUtils.isNotEmpty(lowerVariancePercentString)){ 628 629 KualiDecimal lowerVariancePercent = new KualiDecimal(lowerVariancePercentString); 630 BigDecimal lowerAcceptableVariance = (lowerVariancePercent.divide(new KualiDecimal(100))).multiply(salesTaxAmountCalculated).bigDecimalValue().negate(); 631 632 if (lowerAcceptableVariance.compareTo(BigDecimal.ZERO) >= 0 && 633 actualVariance.compareTo(KualiDecimal.ZERO) >= 0){ 634 if (actualVariance.bigDecimalValue().compareTo(lowerAcceptableVariance) > 0){ 635 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.SALES_TAX_AMT_LESSER_THAN_LOWER_VARIANCE,null,orderHolder.getFileName()); 636 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_TAX_AMT,PurapKeyConstants.ERROR_REJECT_TAXAMOUNT_LOWERVARIANCE); 637 } 638 }else{ 639 if (actualVariance.bigDecimalValue().compareTo(lowerAcceptableVariance) < 0){ 640 ElectronicInvoiceRejectReason rejectReason = createRejectReason(PurapConstants.ElectronicInvoice.SALES_TAX_AMT_LESSER_THAN_LOWER_VARIANCE,null,orderHolder.getFileName()); 641 orderHolder.addInvoiceOrderRejectReason(rejectReason,PurapConstants.ElectronicInvoice.RejectDocumentFields.INVOICE_ITEM_TAX_AMT,PurapKeyConstants.ERROR_REJECT_TAXAMOUNT_LOWERVARIANCE); 642 } 643 } 644 } 645 646 } 647 648 649 650 //Copied from PurApItemBase.calculateExtendedPrice 651 protected BigDecimal getExtendedPrice(ElectronicInvoiceItemHolder itemHolder){ 652 if (itemHolder.getPurchaseOrderItem().getItemType().isAmountBasedGeneralLedgerIndicator()) { 653 // SERVICE ITEM: return unit price as extended price 654 return itemHolder.getUnitPrice(); 655 } 656 else if (ObjectUtils.isNotNull(itemHolder.getQuantity())) { // qty wont be null since it's defined as a reqd field in xsd 657 BigDecimal calcExtendedPrice = itemHolder.getUnitPrice().multiply(itemHolder.getQuantity()); 658 // ITEM TYPE (qty driven): return (unitPrice x qty) 659 return calcExtendedPrice; 660 } 661 return BigDecimal.ZERO; 662 } 663 664 public ElectronicInvoiceRejectReason createRejectReason(String rejectReasonTypeCode, String extraDescription, String fileName) { 665 666 ElectronicInvoiceRejectReasonType rejectReasonType = getElectronicInvoiceRejectReasonType(rejectReasonTypeCode); 667 ElectronicInvoiceRejectReason eInvoiceRejectReason = new ElectronicInvoiceRejectReason(); 668 669 if (rejectReasonType == null){ 670 throw new NullPointerException("Reject reason type for " + rejectReasonTypeCode + " not available in DB"); 671 } 672 eInvoiceRejectReason.setInvoiceFileName(fileName); 673 eInvoiceRejectReason.setInvoiceRejectReasonTypeCode(rejectReasonTypeCode); 674 675 if (StringUtils.isNotEmpty(extraDescription)) { 676 eInvoiceRejectReason.setInvoiceRejectReasonDescription(rejectReasonType.getInvoiceRejectReasonTypeDescription() + " (" + extraDescription + ")"); 677 } 678 else { 679 eInvoiceRejectReason.setInvoiceRejectReasonDescription(rejectReasonType.getInvoiceRejectReasonTypeDescription()); 680 } 681 682 return eInvoiceRejectReason; 683 684 } 685 686 public ElectronicInvoiceRejectReasonType getElectronicInvoiceRejectReasonType(String rejectReasonTypeCode){ 687 if (rejectReasonTypes == null){ 688 rejectReasonTypes = getElectronicInvoiceRejectReasonTypes(); 689 } 690 return rejectReasonTypes.get(rejectReasonTypeCode); 691 } 692 693 protected Map<String, ElectronicInvoiceRejectReasonType> getElectronicInvoiceRejectReasonTypes(){ 694 695 Collection<ElectronicInvoiceRejectReasonType> collection = SpringContext.getBean(BusinessObjectService.class).findAll(ElectronicInvoiceRejectReasonType.class); 696 Map rejectReasonTypesMap = new HashMap<String, ElectronicInvoiceRejectReasonType>(); 697 698 if (collection != null && 699 collection.size() > 0){ 700 ElectronicInvoiceRejectReasonType[] rejectReasonTypesArr = new ElectronicInvoiceRejectReasonType[collection.size()]; 701 collection.toArray(rejectReasonTypesArr); 702 for (int i = 0; i < rejectReasonTypesArr.length; i++) { 703 rejectReasonTypesMap.put(rejectReasonTypesArr[i].getInvoiceRejectReasonTypeCode(), rejectReasonTypesArr[i]); 704 } 705 } 706 707 return rejectReasonTypesMap; 708 } 709 710 public void setVendorService(VendorService vendorService) { 711 this.vendorService = vendorService; 712 } 713 714 public void setTaxService(TaxService taxService) { 715 this.taxService = taxService; 716 } 717 718 public void setDateTimeService(DateTimeService dateTimeService) { 719 this.dateTimeService = dateTimeService; 720 } 721 722 }