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 }