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