001 /* 002 * Copyright 2011 The Kuali Foundation. 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.kfs.module.purap.document.service.impl; 017 018 import java.math.BigDecimal; 019 import java.sql.Date; 020 import java.sql.Timestamp; 021 import java.util.ArrayList; 022 import java.util.Arrays; 023 import java.util.Calendar; 024 import java.util.HashMap; 025 import java.util.Iterator; 026 import java.util.List; 027 import java.util.Map; 028 029 import org.apache.commons.lang.StringUtils; 030 import org.kuali.kfs.module.purap.PurapConstants; 031 import org.kuali.kfs.module.purap.PurapConstants.PurchaseOrderStatuses; 032 import org.kuali.kfs.module.purap.PurapKeyConstants; 033 import org.kuali.kfs.module.purap.PurapParameterConstants; 034 import org.kuali.kfs.module.purap.PurapParameterConstants.TaxParameters; 035 import org.kuali.kfs.module.purap.PurapPropertyConstants; 036 import org.kuali.kfs.module.purap.PurapRuleConstants; 037 import org.kuali.kfs.module.purap.businessobject.AccountsPayableItem; 038 import org.kuali.kfs.module.purap.businessobject.BulkReceivingView; 039 import org.kuali.kfs.module.purap.businessobject.CorrectionReceivingView; 040 import org.kuali.kfs.module.purap.businessobject.CreditMemoView; 041 import org.kuali.kfs.module.purap.businessobject.ItemType; 042 import org.kuali.kfs.module.purap.businessobject.LineItemReceivingView; 043 import org.kuali.kfs.module.purap.businessobject.OrganizationParameter; 044 import org.kuali.kfs.module.purap.businessobject.PaymentRequestView; 045 import org.kuali.kfs.module.purap.businessobject.PurApAccountingLine; 046 import org.kuali.kfs.module.purap.businessobject.PurApItem; 047 import org.kuali.kfs.module.purap.businessobject.PurApItemUseTax; 048 import org.kuali.kfs.module.purap.businessobject.PurapEnterableItem; 049 import org.kuali.kfs.module.purap.businessobject.PurchaseOrderItem; 050 import org.kuali.kfs.module.purap.businessobject.PurchaseOrderView; 051 import org.kuali.kfs.module.purap.businessobject.PurchasingItem; 052 import org.kuali.kfs.module.purap.businessobject.PurchasingItemBase; 053 import org.kuali.kfs.module.purap.businessobject.RequisitionView; 054 import org.kuali.kfs.module.purap.document.AccountsPayableDocument; 055 import org.kuali.kfs.module.purap.document.AccountsPayableDocumentBase; 056 import org.kuali.kfs.module.purap.document.PaymentRequestDocument; 057 import org.kuali.kfs.module.purap.document.PurapItemOperations; 058 import org.kuali.kfs.module.purap.document.PurchaseOrderDocument; 059 import org.kuali.kfs.module.purap.document.PurchasingAccountsPayableDocument; 060 import org.kuali.kfs.module.purap.document.PurchasingDocument; 061 import org.kuali.kfs.module.purap.document.RequisitionDocument; 062 import org.kuali.kfs.module.purap.document.VendorCreditMemoDocument; 063 import org.kuali.kfs.module.purap.document.service.LogicContainer; 064 import org.kuali.kfs.module.purap.document.service.PurapService; 065 import org.kuali.kfs.module.purap.document.service.PurchaseOrderService; 066 import org.kuali.kfs.module.purap.service.PurapAccountingService; 067 import org.kuali.kfs.module.purap.util.PurApItemUtils; 068 import org.kuali.kfs.sys.KFSConstants; 069 import org.kuali.kfs.sys.KFSPropertyConstants; 070 import org.kuali.kfs.sys.businessobject.SourceAccountingLine; 071 import org.kuali.kfs.sys.businessobject.TaxDetail; 072 import org.kuali.kfs.sys.context.SpringContext; 073 import org.kuali.kfs.sys.document.validation.event.DocumentSystemSaveEvent; 074 import org.kuali.kfs.sys.service.NonTransactional; 075 import org.kuali.kfs.sys.service.TaxService; 076 import org.kuali.kfs.sys.service.UniversityDateService; 077 import org.kuali.kfs.sys.service.impl.KfsParameterConstants; 078 import org.kuali.kfs.vnd.businessobject.CommodityCode; 079 import org.kuali.kfs.vnd.businessobject.VendorDetail; 080 import org.kuali.kfs.vnd.document.service.VendorService; 081 import org.kuali.rice.kew.exception.WorkflowException; 082 import org.kuali.rice.kew.service.WorkflowDocumentActions; 083 import org.kuali.rice.kns.UserSession; 084 import org.kuali.rice.kns.bo.Note; 085 import org.kuali.rice.kns.document.Document; 086 import org.kuali.rice.kns.exception.InfrastructureException; 087 import org.kuali.rice.kns.service.BusinessObjectService; 088 import org.kuali.rice.kns.service.DataDictionaryService; 089 import org.kuali.rice.kns.service.DateTimeService; 090 import org.kuali.rice.kns.service.DocumentService; 091 import org.kuali.rice.kns.service.KualiConfigurationService; 092 import org.kuali.rice.kns.service.NoteService; 093 import org.kuali.rice.kns.service.ParameterEvaluator; 094 import org.kuali.rice.kns.service.ParameterService; 095 import org.kuali.rice.kns.service.PersistenceService; 096 import org.kuali.rice.kns.util.GlobalVariables; 097 import org.kuali.rice.kns.util.KNSPropertyConstants; 098 import org.kuali.rice.kns.util.KualiDecimal; 099 import org.kuali.rice.kns.util.ObjectUtils; 100 import org.kuali.rice.kns.workflow.service.KualiWorkflowDocument; 101 102 @NonTransactional 103 public class PurapServiceImpl implements PurapService { 104 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PurapServiceImpl.class); 105 106 private BusinessObjectService businessObjectService; 107 private DataDictionaryService dataDictionaryService; 108 private DateTimeService dateTimeService; 109 private DocumentService documentService; 110 private NoteService noteService; 111 private ParameterService parameterService; 112 private PersistenceService persistenceService; 113 private PurchaseOrderService purchaseOrderService; 114 private UniversityDateService universityDateService; 115 private VendorService vendorService; 116 private TaxService taxService; 117 private PurapAccountingService purapAccountingService; 118 119 public void setBusinessObjectService(BusinessObjectService boService) { 120 this.businessObjectService = boService; 121 } 122 123 public void setDateTimeService(DateTimeService dateTimeService) { 124 this.dateTimeService = dateTimeService; 125 } 126 127 public void setParameterService(ParameterService parameterService) { 128 this.parameterService = parameterService; 129 } 130 131 public void setDocumentService(DocumentService documentService) { 132 this.documentService = documentService; 133 } 134 135 public void setDataDictionaryService(DataDictionaryService dataDictionaryService) { 136 this.dataDictionaryService = dataDictionaryService; 137 } 138 139 public void setVendorService(VendorService vendorService) { 140 this.vendorService = vendorService; 141 } 142 143 public void setPersistenceService(PersistenceService persistenceService) { 144 this.persistenceService = persistenceService; 145 } 146 147 public void setPurchaseOrderService(PurchaseOrderService purchaseOrderService) { 148 this.purchaseOrderService = purchaseOrderService; 149 } 150 151 public void setNoteService(NoteService noteService) { 152 this.noteService = noteService; 153 } 154 155 public void setUniversityDateService(UniversityDateService universityDateService) { 156 this.universityDateService = universityDateService; 157 } 158 159 public void setTaxService(TaxService taxService) { 160 this.taxService = taxService; 161 } 162 163 /** 164 * @see org.kuali.kfs.module.purap.document.service.PurapService#updateStatus(org.kuali.kfs.module.purap.document.PurchasingAccountsPayableDocument, java.lang.String) 165 */ 166 //TODO hjs: is this method really needed now that we don't have status history tables? 167 public boolean updateStatus(PurchasingAccountsPayableDocument document, String newStatus) { 168 LOG.debug("updateStatus() started"); 169 170 if (ObjectUtils.isNotNull(document) || ObjectUtils.isNotNull(newStatus)) { 171 String oldStatus = document.getStatusCode(); 172 document.setStatusCode(newStatus); 173 if ( LOG.isDebugEnabled() ) { 174 LOG.debug("Status of document #" + document.getDocumentNumber() + " has been changed from " + oldStatus + " to " + newStatus); 175 } 176 return true; 177 } 178 else { 179 return false; 180 } 181 } 182 183 public void saveRoutingDataForRelatedDocuments(Integer accountsPayablePurchasingDocumentLinkIdentifier) { 184 185 try { 186 //save requisition routing data 187 List<RequisitionView> reqViews = getRelatedViews(RequisitionView.class, accountsPayablePurchasingDocumentLinkIdentifier); 188 for (Iterator<RequisitionView> iterator = reqViews.iterator(); iterator.hasNext();) { 189 RequisitionView view = (RequisitionView) iterator.next(); 190 Document doc = documentService.getByDocumentHeaderId(view.getDocumentNumber()); 191 doc.getDocumentHeader().getWorkflowDocument().saveRoutingData(); 192 } 193 194 //save purchase order routing data 195 List<PurchaseOrderView> poViews = getRelatedViews(PurchaseOrderView.class, accountsPayablePurchasingDocumentLinkIdentifier); 196 for (Iterator<PurchaseOrderView> iterator = poViews.iterator(); iterator.hasNext();) { 197 PurchaseOrderView view = (PurchaseOrderView) iterator.next(); 198 Document doc = documentService.getByDocumentHeaderId(view.getDocumentNumber()); 199 doc.getDocumentHeader().getWorkflowDocument().saveRoutingData(); 200 } 201 202 //save payment request routing data 203 List<PaymentRequestView> preqViews = getRelatedViews(PaymentRequestView.class, accountsPayablePurchasingDocumentLinkIdentifier); 204 for (Iterator<PaymentRequestView> iterator = preqViews.iterator(); iterator.hasNext();) { 205 PaymentRequestView view = (PaymentRequestView) iterator.next(); 206 Document doc = documentService.getByDocumentHeaderId(view.getDocumentNumber()); 207 doc.getDocumentHeader().getWorkflowDocument().saveRoutingData(); 208 } 209 210 //save credit memo routing data 211 List<CreditMemoView> cmViews = getRelatedViews(CreditMemoView.class, accountsPayablePurchasingDocumentLinkIdentifier); 212 for (Iterator<CreditMemoView> iterator = cmViews.iterator(); iterator.hasNext();) { 213 CreditMemoView view = (CreditMemoView) iterator.next(); 214 Document doc = documentService.getByDocumentHeaderId(view.getDocumentNumber()); 215 doc.getDocumentHeader().getWorkflowDocument().saveRoutingData(); 216 } 217 218 //save line item receiving routing data 219 List<LineItemReceivingView> lineViews = getRelatedViews(LineItemReceivingView.class, accountsPayablePurchasingDocumentLinkIdentifier); 220 for (Iterator<LineItemReceivingView> iterator = lineViews.iterator(); iterator.hasNext();) { 221 LineItemReceivingView view = (LineItemReceivingView) iterator.next(); 222 Document doc = documentService.getByDocumentHeaderId(view.getDocumentNumber()); 223 doc.getDocumentHeader().getWorkflowDocument().saveRoutingData(); 224 } 225 226 //save correction receiving routing data 227 List<CorrectionReceivingView> corrViews = getRelatedViews(CorrectionReceivingView.class, accountsPayablePurchasingDocumentLinkIdentifier); 228 for (Iterator<CorrectionReceivingView> iterator = corrViews.iterator(); iterator.hasNext();) { 229 CorrectionReceivingView view = (CorrectionReceivingView) iterator.next(); 230 Document doc = documentService.getByDocumentHeaderId(view.getDocumentNumber()); 231 doc.getDocumentHeader().getWorkflowDocument().saveRoutingData(); 232 } 233 234 //save bulk receiving routing data 235 List<BulkReceivingView> bulkViews = getRelatedViews(BulkReceivingView.class, accountsPayablePurchasingDocumentLinkIdentifier); 236 for (Iterator<BulkReceivingView> iterator = bulkViews.iterator(); iterator.hasNext();) { 237 BulkReceivingView view = (BulkReceivingView) iterator.next(); 238 Document doc = documentService.getByDocumentHeaderId(view.getDocumentNumber()); 239 doc.getDocumentHeader().getWorkflowDocument().saveRoutingData(); 240 } 241 } 242 catch (WorkflowException e) { 243 throw new InfrastructureException("unable to save routing data for related docs", e); 244 } 245 246 } 247 248 /** 249 * @see org.kuali.kfs.module.purap.document.service.PurapService#getRelatedDocumentIds(java.lang.Integer) 250 */ 251 public List<String> getRelatedDocumentIds(Integer accountsPayablePurchasingDocumentLinkIdentifier) { 252 LOG.debug("getRelatedDocumentIds() started"); 253 List<String> documentIdList = new ArrayList<String>(); 254 255 //get requisition views 256 List<RequisitionView> reqViews = getRelatedViews(RequisitionView.class, accountsPayablePurchasingDocumentLinkIdentifier); 257 for (Iterator<RequisitionView> iterator = reqViews.iterator(); iterator.hasNext();) { 258 RequisitionView view = (RequisitionView) iterator.next(); 259 documentIdList.add(view.getDocumentNumber()); 260 } 261 262 //get purchase order views 263 List<PurchaseOrderView> poViews = getRelatedViews(PurchaseOrderView.class, accountsPayablePurchasingDocumentLinkIdentifier); 264 for (Iterator<PurchaseOrderView> iterator = poViews.iterator(); iterator.hasNext();) { 265 PurchaseOrderView view = (PurchaseOrderView) iterator.next(); 266 documentIdList.add(view.getDocumentNumber()); 267 } 268 269 //get payment request views 270 List<PaymentRequestView> preqViews = getRelatedViews(PaymentRequestView.class, accountsPayablePurchasingDocumentLinkIdentifier); 271 for (Iterator<PaymentRequestView> iterator = preqViews.iterator(); iterator.hasNext();) { 272 PaymentRequestView view = (PaymentRequestView) iterator.next(); 273 documentIdList.add(view.getDocumentNumber()); 274 } 275 276 //get credit memo views 277 List<CreditMemoView> cmViews = getRelatedViews(CreditMemoView.class, accountsPayablePurchasingDocumentLinkIdentifier); 278 for (Iterator<CreditMemoView> iterator = cmViews.iterator(); iterator.hasNext();) { 279 CreditMemoView view = (CreditMemoView) iterator.next(); 280 documentIdList.add(view.getDocumentNumber()); 281 } 282 283 //get line item receiving views 284 List<LineItemReceivingView> lineViews = getRelatedViews(LineItemReceivingView.class, accountsPayablePurchasingDocumentLinkIdentifier); 285 for (Iterator<LineItemReceivingView> iterator = lineViews.iterator(); iterator.hasNext();) { 286 LineItemReceivingView view = (LineItemReceivingView) iterator.next(); 287 documentIdList.add(view.getDocumentNumber()); 288 } 289 290 //get correction receiving views 291 List<CorrectionReceivingView> corrViews = getRelatedViews(CorrectionReceivingView.class, accountsPayablePurchasingDocumentLinkIdentifier); 292 for (Iterator<CorrectionReceivingView> iterator = corrViews.iterator(); iterator.hasNext();) { 293 CorrectionReceivingView view = (CorrectionReceivingView) iterator.next(); 294 documentIdList.add(view.getDocumentNumber()); 295 } 296 297 //get bulk receiving views 298 List<BulkReceivingView> bulkViews = getRelatedViews(BulkReceivingView.class, accountsPayablePurchasingDocumentLinkIdentifier); 299 for (Iterator<BulkReceivingView> iterator = bulkViews.iterator(); iterator.hasNext();) { 300 BulkReceivingView view = (BulkReceivingView) iterator.next(); 301 documentIdList.add(view.getDocumentNumber()); 302 } 303 304 //TODO (hjs)get electronic invoice reject views??? 305 306 return documentIdList; 307 } 308 309 /** 310 * @see org.kuali.kfs.module.purap.document.service.PurapService#getRelatedViews(java.lang.Class, java.lang.Integer) 311 */ 312 @SuppressWarnings("unchecked") 313 public List getRelatedViews(Class clazz, Integer accountsPayablePurchasingDocumentLinkIdentifier) { 314 LOG.debug("getRelatedViews() started"); 315 316 Map criteria = new HashMap(); 317 criteria.put("accountsPayablePurchasingDocumentLinkIdentifier", accountsPayablePurchasingDocumentLinkIdentifier); 318 319 // retrieve in descending order of document number so that newer documents are in the front 320 List boList = (List) businessObjectService.findMatchingOrderBy(clazz, criteria, KFSPropertyConstants.DOCUMENT_NUMBER, false); 321 return boList; 322 } 323 324 /** 325 * @see org.kuali.kfs.module.purap.document.service.PurapService#addBelowLineItems(org.kuali.kfs.module.purap.document.PurchasingAccountsPayableDocument) 326 */ 327 @SuppressWarnings("unchecked") 328 public void addBelowLineItems(PurchasingAccountsPayableDocument document) { 329 LOG.debug("addBelowLineItems() started"); 330 331 String[] itemTypes = getBelowTheLineForDocument(document); 332 333 List<PurApItem> existingItems = document.getItems(); 334 335 List<PurApItem> belowTheLine = new ArrayList<PurApItem>(); 336 // needed in case they get out of sync below won't work 337 sortBelowTheLine(itemTypes, existingItems, belowTheLine); 338 339 List<String> existingItemTypes = new ArrayList<String>(); 340 for (PurApItem existingItem : existingItems) { 341 existingItemTypes.add(existingItem.getItemTypeCode()); 342 } 343 344 Class itemClass = document.getItemClass(); 345 346 for (int i = 0; i < itemTypes.length; i++) { 347 int lastFound; 348 if (!existingItemTypes.contains(itemTypes[i])) { 349 try { 350 if (i > 0) { 351 lastFound = existingItemTypes.lastIndexOf(itemTypes[i - 1]) + 1; 352 } 353 else { 354 lastFound = existingItemTypes.size(); 355 } 356 PurApItem newItem = (PurApItem) itemClass.newInstance(); 357 newItem.setItemTypeCode(itemTypes[i]); 358 newItem.setPurapDocument(document); 359 existingItems.add(lastFound, newItem); 360 existingItemTypes.add(itemTypes[i]); 361 } 362 catch (Exception e) { 363 // do something 364 } 365 } 366 } 367 368 document.fixItemReferences(); 369 } 370 371 /** 372 * Sorts the below the line elements 373 * 374 * @param itemTypes 375 * @param existingItems 376 * @param belowTheLine 377 */ 378 protected void sortBelowTheLine(String[] itemTypes, List<PurApItem> existingItems, List<PurApItem> belowTheLine) { 379 LOG.debug("sortBelowTheLine() started"); 380 381 // sort existing below the line if any 382 for (int i = 0; i < existingItems.size(); i++) { 383 PurApItem purApItem = existingItems.get(i); 384 if (purApItem.getItemType().isAdditionalChargeIndicator()) { 385 belowTheLine.add(existingItems.get(i)); 386 } 387 } 388 existingItems.removeAll(belowTheLine); 389 for (int i = 0; i < itemTypes.length; i++) { 390 for (PurApItem purApItem : belowTheLine) { 391 if (StringUtils.equalsIgnoreCase(purApItem.getItemTypeCode(), itemTypes[i])) { 392 existingItems.add(purApItem); 393 break; 394 } 395 } 396 } 397 belowTheLine.removeAll(existingItems); 398 if (belowTheLine.size() != 0) { 399 throw new RuntimeException("below the line item sort didn't work: trying to remove an item without adding it back"); 400 } 401 } 402 403 /** 404 * @see org.kuali.kfs.module.purap.document.service.PurapService#sortBelowTheLine(java.lang.String[], java.util.List, java.util.List) 405 */ 406 public void sortBelowTheLine(PurchasingAccountsPayableDocument document) { 407 LOG.debug("sortBelowTheLine() started"); 408 409 String[] itemTypes = getBelowTheLineForDocument(document); 410 411 List<PurApItem> existingItems = document.getItems(); 412 413 List<PurApItem> belowTheLine = new ArrayList<PurApItem>(); 414 // needed in case they get out of sync below won't work 415 sortBelowTheLine(itemTypes, existingItems, belowTheLine); 416 } 417 418 /** 419 * @see org.kuali.kfs.module.purap.document.service.PurapService#getBelowTheLineForDocument(org.kuali.kfs.module.purap.document.PurchasingAccountsPayableDocument) 420 */ 421 public String[] getBelowTheLineForDocument(PurchasingAccountsPayableDocument document) { 422 LOG.debug("getBelowTheLineForDocument() started"); 423 424 // Obtain a list of below the line items from system parameter 425 // String documentTypeClassName = document.getClass().getName(); 426 // String[] documentTypeArray = StringUtils.split(documentTypeClassName, "."); 427 // String documentType = documentTypeArray[documentTypeArray.length - 1]; 428 429 //FIXME RELEASE 3 (hjs) why is this "if" here with no code in it? is it supposed to be doing somethign? 430 // If it's a credit memo, we'll have to append the source of the credit memo 431 // whether it's created from a Vendor, a PO or a PREQ. 432 // if (documentType.equals("CreditMemoDocument")) { 433 // 434 // } 435 436 String documentType = dataDictionaryService.getDocumentTypeNameByClass(document.getClass()); 437 438 try { 439 return parameterService.getParameterValues(Class.forName(PurapConstants.PURAP_DETAIL_TYPE_CODE_MAP.get(documentType)), PurapConstants.BELOW_THE_LINES_PARAMETER).toArray(new String[] {}); 440 } 441 catch (ClassNotFoundException e) { 442 throw new RuntimeException("The getBelowTheLineForDocument method of PurapServiceImpl was unable to resolve the document class for type: " + PurapConstants.PURAP_DETAIL_TYPE_CODE_MAP.get(documentType), e); 443 } 444 } 445 446 /** 447 * @see org.kuali.kfs.module.purap.document.service.PurapService#getBelowTheLineByType(org.kuali.kfs.module.purap.document.PurchasingAccountsPayableDocument, 448 * org.kuali.kfs.module.purap.businessobject.ItemType) 449 */ 450 public PurApItem getBelowTheLineByType(PurchasingAccountsPayableDocument document, ItemType iT) { 451 LOG.debug("getBelowTheLineByType() started"); 452 453 String[] itemTypes = getBelowTheLineForDocument(document); 454 boolean foundItemType = false; 455 for (String itemType : itemTypes) { 456 if (StringUtils.equals(iT.getItemTypeCode(), itemType)) { 457 foundItemType = true; 458 break; 459 } 460 } 461 if (!foundItemType) { 462 return null; 463 } 464 465 PurApItem belowTheLineItem = null; 466 for (PurApItem item : (List<PurApItem>) document.getItems()) { 467 if (item.getItemType().isAdditionalChargeIndicator()) { 468 if (StringUtils.equals(iT.getItemTypeCode(), item.getItemType().getItemTypeCode())) { 469 belowTheLineItem = item; 470 break; 471 } 472 } 473 } 474 return belowTheLineItem; 475 } 476 477 /** 478 * @see org.kuali.kfs.module.purap.document.service.PurapService#getDateFromOffsetFromToday(int) 479 */ 480 public java.sql.Date getDateFromOffsetFromToday(int offsetDays) { 481 Calendar calendar = dateTimeService.getCurrentCalendar(); 482 calendar.add(Calendar.DATE, offsetDays); 483 return new java.sql.Date(calendar.getTimeInMillis()); 484 } 485 486 /** 487 * @see org.kuali.kfs.module.purap.document.service.PurapService#isDateInPast(java.sql.Date) 488 */ 489 public boolean isDateInPast(Date compareDate) { 490 LOG.debug("isDateInPast() started"); 491 492 Date today = dateTimeService.getCurrentSqlDate(); 493 int diffFromToday = dateTimeService.dateDiff(today, compareDate, false); 494 return (diffFromToday < 0); 495 } 496 497 /** 498 * @see org.kuali.kfs.module.purap.document.service.PurapService#isDateMoreThanANumberOfDaysAway(java.sql.Date, int) 499 */ 500 public boolean isDateMoreThanANumberOfDaysAway(Date compareDate, int daysAway) { 501 LOG.debug("isDateMoreThanANumberOfDaysAway() started"); 502 503 Date todayAtMidnight = dateTimeService.getCurrentSqlDateMidnight(); 504 Calendar daysAwayCalendar = dateTimeService.getCalendar(todayAtMidnight); 505 daysAwayCalendar.add(Calendar.DATE, daysAway); 506 Timestamp daysAwayTime = new Timestamp(daysAwayCalendar.getTime().getTime()); 507 Calendar compareCalendar = dateTimeService.getCalendar(compareDate); 508 compareCalendar.set(Calendar.HOUR, 0); 509 compareCalendar.set(Calendar.MINUTE, 0); 510 compareCalendar.set(Calendar.SECOND, 0); 511 compareCalendar.set(Calendar.MILLISECOND, 0); 512 compareCalendar.set(Calendar.AM_PM, Calendar.AM); 513 Timestamp compareTime = new Timestamp(compareCalendar.getTime().getTime()); 514 return (compareTime.compareTo(daysAwayTime) > 0); 515 } 516 517 /** 518 * @see org.kuali.kfs.module.purap.document.service.PurapService#isDateAYearAfterToday(java.sql.Date) 519 */ 520 public boolean isDateAYearBeforeToday(Date compareDate) { 521 LOG.debug("isDateAYearBeforeToday() started"); 522 523 Calendar calendar = dateTimeService.getCurrentCalendar(); 524 calendar.add(Calendar.YEAR, -1); 525 java.sql.Date yearAgo = new java.sql.Date(calendar.getTimeInMillis()); 526 int diffFromYearAgo = dateTimeService.dateDiff(compareDate, yearAgo, false); 527 return (diffFromYearAgo > 0); 528 } 529 530 /** 531 * @see org.kuali.kfs.module.purap.document.service.PurapService#getApoLimit(java.lang.Integer, java.lang.String, java.lang.String) 532 */ 533 @SuppressWarnings("unchecked") 534 public KualiDecimal getApoLimit(Integer vendorContractGeneratedIdentifier, String chart, String org) { 535 LOG.debug("getApoLimit() started"); 536 537 KualiDecimal purchaseOrderTotalLimit = vendorService.getApoLimitFromContract(vendorContractGeneratedIdentifier, chart, org); 538 539 // We didn't find the limit on the vendor contract, get it from the org parameter table. 540 if (ObjectUtils.isNull(purchaseOrderTotalLimit) && !ObjectUtils.isNull(chart) && !ObjectUtils.isNull(org)) { 541 OrganizationParameter organizationParameter = new OrganizationParameter(); 542 organizationParameter.setChartOfAccountsCode(chart); 543 organizationParameter.setOrganizationCode(org); 544 Map orgParamKeys = persistenceService.getPrimaryKeyFieldValues(organizationParameter); 545 orgParamKeys.put(KNSPropertyConstants.ACTIVE_INDICATOR, true); 546 organizationParameter = (OrganizationParameter) businessObjectService.findByPrimaryKey(OrganizationParameter.class, orgParamKeys); 547 purchaseOrderTotalLimit = (organizationParameter == null) ? null : organizationParameter.getOrganizationAutomaticPurchaseOrderLimit(); 548 } 549 550 if (ObjectUtils.isNull(purchaseOrderTotalLimit)) { 551 String defaultLimit = parameterService.getParameterValue(RequisitionDocument.class, PurapParameterConstants.AUTOMATIC_PURCHASE_ORDER_DEFAULT_LIMIT_AMOUNT); 552 purchaseOrderTotalLimit = new KualiDecimal(defaultLimit); 553 } 554 555 return purchaseOrderTotalLimit; 556 } 557 558 /** 559 * @see org.kuali.kfs.module.purap.document.service.PurapService#isFullDocumentEntryCompleted(org.kuali.kfs.module.purap.document.PurchasingAccountsPayableDocument) 560 */ 561 public boolean isFullDocumentEntryCompleted(PurchasingAccountsPayableDocument purapDocument) { 562 LOG.debug("isFullDocumentEntryCompleted() started"); 563 564 // for now just return true if not in one of the first few states 565 boolean value = false; 566 if (purapDocument instanceof PaymentRequestDocument) { 567 value = PurapConstants.PaymentRequestStatuses.STATUS_ORDER.isFullDocumentEntryCompleted(purapDocument.getStatusCode()); 568 } 569 else if (purapDocument instanceof VendorCreditMemoDocument) { 570 value = PurapConstants.CreditMemoStatuses.STATUS_ORDER.isFullDocumentEntryCompleted(purapDocument.getStatusCode()); 571 } 572 return value; 573 } 574 575 576 /** 577 * Main hook point for close/Reopen PO. 578 * 579 * @see org.kuali.kfs.module.purap.document.service.PurapService#performLogicForCloseReopenPO(org.kuali.kfs.module.purap.document.PurchasingAccountsPayableDocument) 580 */ 581 public void performLogicForCloseReopenPO(PurchasingAccountsPayableDocument purapDocument) { 582 LOG.debug("performLogicForCloseReopenPO() started"); 583 584 if (purapDocument instanceof PaymentRequestDocument) { 585 PaymentRequestDocument paymentRequest = (PaymentRequestDocument) purapDocument; 586 587 if (paymentRequest.isClosePurchaseOrderIndicator() && PurapConstants.PurchaseOrderStatuses.OPEN.equals(paymentRequest.getPurchaseOrderDocument().getStatusCode())) { 588 // get the po id and get the current po 589 // check the current po: if status is not closed and there is no pending action... route close po as system user 590 processCloseReopenPo((AccountsPayableDocumentBase) purapDocument, PurapConstants.PurchaseOrderDocTypes.PURCHASE_ORDER_CLOSE_DOCUMENT); 591 } 592 593 } 594 else if (purapDocument instanceof VendorCreditMemoDocument) { 595 VendorCreditMemoDocument creditMemo = (VendorCreditMemoDocument) purapDocument; 596 597 if (creditMemo.isReopenPurchaseOrderIndicator() && PurapConstants.PurchaseOrderStatuses.CLOSED.equals(creditMemo.getPurchaseOrderDocument().getStatusCode())) { 598 // get the po id and get the current PO 599 // route 'Re-Open PO Document' if PO criteria meets requirements from business rules 600 processCloseReopenPo((AccountsPayableDocumentBase) purapDocument, PurapConstants.PurchaseOrderDocTypes.PURCHASE_ORDER_REOPEN_DOCUMENT); 601 } 602 603 } 604 else { 605 throw new RuntimeException("Attempted to perform full entry logic for unhandled document type '" + purapDocument.getClass().getName() + "'"); 606 } 607 608 } 609 610 /** 611 * Remove items that have not been "entered" which means no data has been added to them so no more processing needs to continue 612 * on these items. 613 * 614 * @param apDocument AccountsPayableDocument which contains list of items to be reviewed 615 */ 616 public void deleteUnenteredItems(PurapItemOperations document) { 617 LOG.debug("deleteUnenteredItems() started"); 618 619 List<PurapEnterableItem> deletionList = new ArrayList<PurapEnterableItem>(); 620 for (PurapEnterableItem item : (List<PurapEnterableItem>) document.getItems()) { 621 if (!item.isConsideredEntered()) { 622 deletionList.add(item); 623 } 624 } 625 document.getItems().removeAll(deletionList); 626 } 627 628 /** 629 * Actual method that will close or reopen a po. 630 * 631 * @param apDocument AccountsPayableDocument 632 * @param docType 633 */ 634 @SuppressWarnings("unchecked") 635 public void processCloseReopenPo(AccountsPayableDocumentBase apDocument, String docType) { 636 LOG.debug("processCloseReopenPo() started"); 637 638 String action = null; 639 String newStatus = null; 640 // setup text for note that will be created, will either be closed or reopened 641 if (PurapConstants.PurchaseOrderDocTypes.PURCHASE_ORDER_CLOSE_DOCUMENT.equals(docType)) { 642 action = "closed"; 643 newStatus = PurchaseOrderStatuses.PENDING_CLOSE; 644 } 645 else if (PurapConstants.PurchaseOrderDocTypes.PURCHASE_ORDER_REOPEN_DOCUMENT.equals(docType)) { 646 action = "reopened"; 647 newStatus = PurchaseOrderStatuses.PENDING_REOPEN; 648 } 649 else { 650 String errorMessage = "Method processCloseReopenPo called using ID + '" + apDocument.getPurapDocumentIdentifier() + "' and invalid doc type '" + docType + "'"; 651 LOG.error(errorMessage); 652 throw new RuntimeException(errorMessage); 653 } 654 655 656 Integer poId = apDocument.getPurchaseOrderIdentifier(); 657 PurchaseOrderDocument purchaseOrderDocument = purchaseOrderService.getCurrentPurchaseOrder(poId); 658 if (!StringUtils.equalsIgnoreCase(purchaseOrderDocument.getDocumentHeader().getWorkflowDocument().getDocumentType(), docType)) { 659 // we are skipping the validation above because it would be too late to correct any errors (i.e. because in 660 // post-processing) 661 purchaseOrderService.createAndRoutePotentialChangeDocument(purchaseOrderDocument.getDocumentNumber(), docType, assemblePurchaseOrderNote(apDocument, docType, action), new ArrayList(), newStatus); 662 } 663 664 /* 665 * if we made it here, route document has not errored out, so set appropriate indicator depending on what is being 666 * requested. 667 */ 668 if (PurapConstants.PurchaseOrderDocTypes.PURCHASE_ORDER_CLOSE_DOCUMENT.equals(docType)) { 669 apDocument.setClosePurchaseOrderIndicator(false); 670 671 //add a note to the purchase order indicating it has been closed by a payment request document 672 String userName = apDocument.getLastActionPerformedByPersonName(); 673 StringBuffer poNote = new StringBuffer(""); 674 poNote.append("PO was closed manually by "); 675 poNote.append( userName ); 676 poNote.append(" in approving PREQ with ID "); 677 poNote.append(apDocument.getDocumentNumber()); 678 679 //save the note to the purchase order 680 try{ 681 Note noteObj = documentService.createNoteFromDocument(apDocument.getPurchaseOrderDocument(), poNote.toString()); 682 documentService.addNoteToDocument(apDocument.getPurchaseOrderDocument(), noteObj); 683 noteService.save(noteObj); 684 }catch(Exception e){ 685 String errorMessage = "Error creating and saving close note for purchase order with document service"; 686 LOG.error("processCloseReopenPo() " + errorMessage, e); 687 throw new RuntimeException(errorMessage, e); 688 } 689 } 690 else if (PurapConstants.PurchaseOrderDocTypes.PURCHASE_ORDER_REOPEN_DOCUMENT.equals(docType)) { 691 apDocument.setReopenPurchaseOrderIndicator(false); 692 } 693 694 } 695 696 /** 697 * Generate a note for the close/reopen po method. 698 * 699 * @param docType 700 * @param preqId 701 * @return Note to be saved 702 */ 703 protected String assemblePurchaseOrderNote(AccountsPayableDocumentBase apDocument, String docType, String action) { 704 LOG.debug("assemblePurchaseOrderNote() started"); 705 706 String documentLabel = dataDictionaryService.getDocumentLabelByClass(apDocument.getClass()); 707 StringBuffer closeReopenNote = new StringBuffer(""); 708 String userName = GlobalVariables.getUserSession().getPerson().getName(); 709 closeReopenNote.append(dataDictionaryService.getDocumentLabelByTypeName(KFSConstants.FinancialDocumentTypeCodes.PURCHASE_ORDER)); 710 closeReopenNote.append(" will be manually "); 711 closeReopenNote.append(action); 712 closeReopenNote.append(" by "); 713 closeReopenNote.append(userName); 714 closeReopenNote.append(" when approving "); 715 closeReopenNote.append(documentLabel); 716 closeReopenNote.append(" with "); 717 closeReopenNote.append(dataDictionaryService.getAttributeLabel(apDocument.getClass(), PurapPropertyConstants.PURAP_DOC_ID)); 718 closeReopenNote.append(" "); 719 closeReopenNote.append(apDocument.getPurapDocumentIdentifier()); 720 721 return closeReopenNote.toString(); 722 } 723 724 /** 725 * @see org.kuali.kfs.module.purap.document.service.PurapService#performLogicWithFakedUserSession(java.lang.String, org.kuali.kfs.module.purap.document.service.LogicContainer, java.lang.Object[]) 726 */ 727 public Object performLogicWithFakedUserSession(String requiredPersonPersonUserId, LogicContainer logicToRun, Object... objects) throws WorkflowException, Exception { 728 LOG.debug("performLogicWithFakedUserSession() started"); 729 730 if (StringUtils.isBlank(requiredPersonPersonUserId)) { 731 throw new RuntimeException("Attempted to perform logic with a fake user session with a blank user person id: '" + requiredPersonPersonUserId + "'"); 732 } 733 if (ObjectUtils.isNull(logicToRun)) { 734 throw new RuntimeException("Attempted to perform logic with a fake user session with no logic to run"); 735 } 736 UserSession actualUserSession = GlobalVariables.getUserSession(); 737 try { 738 GlobalVariables.setUserSession(new UserSession(requiredPersonPersonUserId)); 739 return logicToRun.runLogic(objects); 740 } 741 finally { 742 GlobalVariables.setUserSession(actualUserSession); 743 } 744 } 745 746 /** 747 * @see org.kuali.kfs.module.purap.document.service.PurchaseOrderService#saveDocumentNoValidation(org.kuali.kfs.module.purap.document.PurchaseOrderDocument) 748 */ 749 public void saveDocumentNoValidation(Document document) { 750 try { 751 // FIXME The following code of refreshing document header is a temporary fix for the issue that 752 // in some cases (seem random) the doc header fields are null; and if doc header is refreshed, the workflow doc becomes null. 753 // The root cause of this is that when some docs are retrieved manually using OJB criteria, ref objs such as doc header or workflow doc 754 // aren't retrieved; the solution would be to add these refreshing when documents are retrieved in those OJB methods. 755 if (document.getDocumentHeader() != null && document.getDocumentHeader().getDocumentNumber() == null) { 756 KualiWorkflowDocument workflowDocument = document.getDocumentHeader().getWorkflowDocument(); 757 document.refreshReferenceObject("documentHeader"); 758 document.getDocumentHeader().setWorkflowDocument(workflowDocument); 759 } 760 documentService.saveDocument(document, DocumentSystemSaveEvent.class); 761 762 // At this point, the work-flow status will not change for the current document, but the document status will. 763 // This causes the search indices for the document to become out of synch, and will show a different status type 764 // in the RICE lookup results screen. 765 WorkflowDocumentActions wrkflowDocActions = SpringContext.getBean(WorkflowDocumentActions.class); 766 wrkflowDocActions.indexDocument(Long.valueOf(document.getDocumentNumber())); 767 } 768 catch (WorkflowException we) { 769 String errorMsg = "Workflow error saving document # " + document.getDocumentHeader().getDocumentNumber() + " " + we.getMessage(); 770 LOG.error(errorMsg, we); 771 throw new RuntimeException(errorMsg, we); 772 } 773 catch (NumberFormatException ne) { 774 String errorMsg = "Invalid document number format for document # " + document.getDocumentHeader().getDocumentNumber() + " " + ne.getMessage(); 775 LOG.error(errorMsg, ne); 776 throw new RuntimeException(errorMsg, ne); 777 } 778 } 779 780 public boolean isDocumentStoppedInRouteNode(PurchasingAccountsPayableDocument document, String nodeName) { 781 List<String> currentRouteLevels = new ArrayList<String>(); 782 try { 783 KualiWorkflowDocument workflowDoc = document.getDocumentHeader().getWorkflowDocument(); 784 currentRouteLevels = Arrays.asList(document.getDocumentHeader().getWorkflowDocument().getNodeNames()); 785 if (currentRouteLevels.contains(nodeName) && workflowDoc.isApprovalRequested()) { 786 return true; 787 } 788 return false; 789 } 790 catch (WorkflowException e) { 791 throw new RuntimeException(e); 792 } 793 } 794 795 /** 796 * @see org.kuali.kfs.module.purap.document.service.PurapService#allowEncumberNextFiscalYear() 797 */ 798 public boolean allowEncumberNextFiscalYear() { 799 LOG.debug("allowEncumberNextFiscalYear() started"); 800 801 java.util.Date today = dateTimeService.getCurrentDate(); 802 java.util.Date closingDate = universityDateService.getLastDateOfFiscalYear(universityDateService.getCurrentFiscalYear()); 803 int allowEncumberNext = (Integer.parseInt(parameterService.getParameterValue(RequisitionDocument.class, PurapRuleConstants.ALLOW_ENCUMBER_NEXT_YEAR_DAYS))); 804 int diffTodayClosing = dateTimeService.dateDiff(today, closingDate, false); 805 806 if (ObjectUtils.isNotNull(closingDate) && ObjectUtils.isNotNull(today) && ObjectUtils.isNotNull(allowEncumberNext)) { 807 if ( LOG.isDebugEnabled() ) { 808 LOG.debug("allowEncumberNextFiscalYear() today = " + dateTimeService.toDateString(today) + "; encumber next FY range = " + allowEncumberNext + " - " + dateTimeService.toDateTimeString(today)); 809 } 810 811 if (allowEncumberNext >= diffTodayClosing && diffTodayClosing >= KualiDecimal.ZERO.intValue()) { 812 LOG.debug("allowEncumberNextFiscalYear() encumber next FY allowed; return true."); 813 return true; 814 } 815 } 816 LOG.debug("allowEncumberNextFiscalYear() encumber next FY not allowed; return false."); 817 return false; 818 } 819 820 /** 821 * @see org.kuali.kfs.module.purap.document.service.PurapService#getAllowedFiscalYears() 822 */ 823 public List<Integer> getAllowedFiscalYears() { 824 List<Integer> allowedYears = new ArrayList<Integer>(); 825 Integer currentFY = universityDateService.getCurrentFiscalYear(); 826 allowedYears.add(currentFY); 827 if (allowEncumberNextFiscalYear()) { 828 allowedYears.add(currentFY + 1); 829 } 830 return allowedYears; 831 } 832 833 /** 834 * @see org.kuali.kfs.module.purap.document.service.PurapService#isTodayWithinApoAllowedRange() 835 */ 836 public boolean isTodayWithinApoAllowedRange() { 837 java.util.Date today = dateTimeService.getCurrentDate(); 838 Integer currentFY = universityDateService.getCurrentFiscalYear(); 839 java.util.Date closingDate = universityDateService.getLastDateOfFiscalYear(currentFY); 840 int allowApoDate = (Integer.parseInt(parameterService.getParameterValue(RequisitionDocument.class, PurapRuleConstants.ALLOW_APO_NEXT_FY_DAYS))); 841 int diffTodayClosing = dateTimeService.dateDiff(today, closingDate, true); 842 843 return diffTodayClosing <= allowApoDate; 844 } 845 846 /** 847 * 848 * @see org.kuali.kfs.module.purap.document.service.PurapService#clearTax(org.kuali.kfs.module.purap.document.PurchasingAccountsPayableDocument) 849 */ 850 public void clearTax(PurchasingAccountsPayableDocument purapDocument, boolean useTax){ 851 for (PurApItem item : purapDocument.getItems()) { 852 if(useTax) { 853 item.getUseTaxItems().clear(); 854 } else { 855 item.setItemTaxAmount(null); 856 } 857 } 858 } 859 860 public void updateUseTaxIndicator(PurchasingAccountsPayableDocument purapDocument, boolean newUseTaxIndicatorValue) { 861 boolean currentUseTaxIndicator = purapDocument.isUseTaxIndicator(); 862 if(currentUseTaxIndicator!=newUseTaxIndicatorValue) { 863 //i.e. if the indicator changed clear out the tax 864 clearTax(purapDocument, currentUseTaxIndicator); 865 } 866 purapDocument.setUseTaxIndicator(newUseTaxIndicatorValue); 867 } 868 869 /** 870 * @see org.kuali.kfs.module.purap.document.service.PurapService#calculateTax(org.kuali.kfs.module.purap.document.PurchasingAccountsPayableDocument) 871 */ 872 public void calculateTax(PurchasingAccountsPayableDocument purapDocument){ 873 874 boolean salesTaxInd = SpringContext.getBean(ParameterService.class).getIndicatorParameter(KfsParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.ENABLE_SALES_TAX_IND); 875 boolean useTaxIndicator = purapDocument.isUseTaxIndicator(); 876 String deliveryState = getDeliveryState(purapDocument); 877 String deliveryPostalCode = getDeliveryPostalCode(purapDocument); 878 Date transactionTaxDate = purapDocument.getTransactionTaxDate(); 879 880 //calculate if sales tax enabled for purap 881 if( salesTaxInd || useTaxIndicator ){ 882 //iterate over items and calculate tax if taxable 883 for(PurApItem item : purapDocument.getItems()){ 884 if( isTaxable(useTaxIndicator, deliveryState, item) ){ 885 calculateItemTax(useTaxIndicator, deliveryPostalCode, transactionTaxDate, item, item.getUseTaxClass(), purapDocument); 886 } 887 } 888 } 889 } 890 891 public String getDeliveryState(PurchasingAccountsPayableDocument purapDocument){ 892 if (purapDocument instanceof PurchasingDocument){ 893 PurchasingDocument document = (PurchasingDocument)purapDocument; 894 return document.getDeliveryStateCode(); 895 }else if (purapDocument instanceof AccountsPayableDocument){ 896 AccountsPayableDocument document = (AccountsPayableDocument)purapDocument; 897 if (document.getPurchaseOrderDocument() == null){ 898 throw new RuntimeException("PurchaseOrder document does not exists"); 899 } 900 return document.getPurchaseOrderDocument().getDeliveryStateCode(); 901 } 902 return null; 903 } 904 905 protected String getDeliveryPostalCode(PurchasingAccountsPayableDocument purapDocument){ 906 if (purapDocument instanceof PurchasingDocument){ 907 PurchasingDocument document = (PurchasingDocument)purapDocument; 908 return document.getDeliveryPostalCode(); 909 }else if (purapDocument instanceof AccountsPayableDocument){ 910 AccountsPayableDocument docBase = (AccountsPayableDocument)purapDocument; 911 if (docBase.getPurchaseOrderDocument() == null){ 912 throw new RuntimeException("PurchaseOrder document does not exists"); 913 } 914 return docBase.getPurchaseOrderDocument().getDeliveryPostalCode(); 915 } 916 return null; 917 } 918 919 /** 920 * Determines if the item is taxable based on a decision tree. 921 * 922 * @param useTaxIndicator 923 * @param deliveryState 924 * @param item 925 * @return 926 */ 927 public boolean isTaxable(boolean useTaxIndicator, String deliveryState, PurApItem item){ 928 929 boolean taxable = false; 930 931 if(item.getItemType().isTaxableIndicator() && 932 ((ObjectUtils.isNull(item.getItemTaxAmount()) && useTaxIndicator == false) || useTaxIndicator) && 933 (doesCommodityAllowCallToTaxService(item)) && 934 (doesAccountAllowCallToTaxService(deliveryState, item)) ){ 935 936 taxable = true; 937 } 938 return taxable; 939 } 940 941 /** 942 * 943 * @see org.kuali.kfs.module.purap.document.service.PurapService#isTaxableForSummary(boolean, java.lang.String, org.kuali.kfs.module.purap.businessobject.PurApItem) 944 */ 945 public boolean isTaxableForSummary(boolean useTaxIndicator, String deliveryState, PurApItem item){ 946 947 boolean taxable = false; 948 949 if(item.getItemType().isTaxableIndicator() && 950 (doesCommodityAllowCallToTaxService(item)) && 951 (doesAccountAllowCallToTaxService(deliveryState, item)) ){ 952 953 taxable = true; 954 } 955 return taxable; 956 } 957 958 /** 959 * Determines if the the tax service should be called due to the commodity code. 960 * 961 * @param item 962 * @return 963 */ 964 protected boolean doesCommodityAllowCallToTaxService(PurApItem item) { 965 boolean callService = true; 966 967 // only check for commodity code on above the line times (additional charges don't allow commodity code) 968 if (item.getItemType().isLineItemIndicator()) { 969 if (item instanceof PurchasingItem) { 970 PurchasingItemBase purItem = (PurchasingItemBase) item; 971 callService = isCommodityCodeTaxable(purItem.getCommodityCode()); 972 }// if not a purchasing item, then pull item from PO 973 else if (item instanceof AccountsPayableItem) { 974 AccountsPayableItem apItem = (AccountsPayableItem) item; 975 PurchaseOrderItem poItem = apItem.getPurchaseOrderItem(); 976 if (ObjectUtils.isNotNull(poItem)) { 977 callService = isCommodityCodeTaxable(poItem.getCommodityCode()); 978 } 979 } 980 } 981 982 return callService; 983 } 984 985 protected boolean isCommodityCodeTaxable(CommodityCode commodityCode){ 986 boolean isTaxable = true; 987 988 if(ObjectUtils.isNotNull(commodityCode)){ 989 990 if(commodityCode.isSalesTaxIndicator() == false){ 991 //not taxable, so don't call service 992 isTaxable = false; 993 }//if true we want to call service 994 995 }//if null, return true 996 997 return isTaxable; 998 } 999 1000 /** 1001 * @see org.kuali.kfs.module.purap.document.service.PurapService#isDeliveryStateTaxable(java.lang.String) 1002 */ 1003 public boolean isDeliveryStateTaxable(String deliveryState) { 1004 ParameterEvaluator parmEval = SpringContext.getBean(ParameterService.class).getParameterEvaluator(KfsParameterConstants.PURCHASING_DOCUMENT.class, TaxParameters.TAXABLE_DELIVERY_STATES, deliveryState); 1005 return parmEval.evaluationSucceeds(); 1006 } 1007 1008 /** 1009 * Checks if the account is taxable, based on the delivery state, fund/subfund groups, and object code level/consolidations. 1010 * 1011 * @param deliveryState 1012 * @param item 1013 * @return 1014 */ 1015 protected boolean doesAccountAllowCallToTaxService(String deliveryState, PurApItem item) { 1016 boolean callService = false; 1017 boolean deliveryStateTaxable = isDeliveryStateTaxable(deliveryState); 1018 1019 for (PurApAccountingLine acctLine : item.getSourceAccountingLines()) { 1020 if(isAccountingLineTaxable(acctLine, deliveryStateTaxable)){ 1021 callService = true; 1022 break; 1023 } 1024 } 1025 1026 return callService; 1027 } 1028 1029 /** 1030 * @see org.kuali.kfs.module.purap.document.service.PurapService#isAccountingLineTaxable(org.kuali.kfs.module.purap.businessobject.PurApAccountingLine, boolean) 1031 */ 1032 public boolean isAccountingLineTaxable(PurApAccountingLine acctLine, boolean deliveryStateTaxable){ 1033 boolean isTaxable = false; 1034 String parameterSuffix = null; 1035 1036 if (deliveryStateTaxable) { 1037 parameterSuffix = TaxParameters.FOR_TAXABLE_STATES_SUFFIX; 1038 } 1039 else { 1040 parameterSuffix = TaxParameters.FOR_NON_TAXABLE_STATES_SUFFIX; 1041 } 1042 1043 // is account (fund/subfund) and object code (level/consolidation) taxable? 1044 if (isAccountTaxable(parameterSuffix, acctLine) && isObjectCodeTaxable(parameterSuffix, acctLine)) { 1045 isTaxable = true; 1046 } 1047 1048 return isTaxable; 1049 } 1050 1051 /** 1052 * Checks if the account fund/subfund groups are in a set of parameters taking into account allowed/denied constraints and 1053 * ultimately determines if taxable. 1054 * 1055 * @param parameterSuffix 1056 * @param acctLine 1057 * @return 1058 */ 1059 protected boolean isAccountTaxable(String parameterSuffix, PurApAccountingLine acctLine){ 1060 1061 boolean isAccountTaxable = false; 1062 String fundParam = TaxParameters.TAXABLE_FUND_GROUPS_PREFIX + parameterSuffix; 1063 String subFundParam = TaxParameters.TAXABLE_SUB_FUND_GROUPS_PREFIX + parameterSuffix; 1064 ParameterEvaluator fundParamEval = null; 1065 ParameterEvaluator subFundParamEval = null; 1066 1067 if (ObjectUtils.isNull(acctLine.getAccount().getSubFundGroup())){ 1068 acctLine.refreshNonUpdateableReferences(); 1069 } 1070 1071 fundParamEval = SpringContext.getBean(ParameterService.class).getParameterEvaluator(KfsParameterConstants.PURCHASING_DOCUMENT.class, fundParam, acctLine.getAccount().getSubFundGroup().getFundGroupCode()); 1072 subFundParamEval = SpringContext.getBean(ParameterService.class).getParameterEvaluator(KfsParameterConstants.PURCHASING_DOCUMENT.class, subFundParam, acctLine.getAccount().getSubFundGroupCode()); 1073 1074 if( (isAllowedFound(fundParamEval) && (isAllowedFound(subFundParamEval) || isAllowedNotFound(subFundParamEval) || isDeniedNotFound(subFundParamEval))) || 1075 (isAllowedNotFound(fundParamEval) && isAllowedFound(subFundParamEval)) || 1076 (isDeniedFound(fundParamEval) && isAllowedFound(subFundParamEval)) || 1077 (isDeniedNotFound(fundParamEval) && (isAllowedFound(subFundParamEval) || isAllowedNotFound(subFundParamEval) || isDeniedNotFound(subFundParamEval))) ){ 1078 1079 isAccountTaxable = true; 1080 } 1081 1082 return isAccountTaxable; 1083 } 1084 1085 /** 1086 * Checks if the object code level/consolidation groups are in a set of parameters taking into account allowed/denied constraints and 1087 * ultimately determines if taxable. 1088 * 1089 * @param parameterSuffix 1090 * @param acctLine 1091 * @return 1092 */ 1093 protected boolean isObjectCodeTaxable(String parameterSuffix, PurApAccountingLine acctLine){ 1094 1095 boolean isObjectCodeTaxable = false; 1096 String levelParam = TaxParameters.TAXABLE_OBJECT_LEVELS_PREFIX + parameterSuffix; 1097 String consolidationParam = TaxParameters.TAXABLE_OBJECT_CONSOLIDATIONS_PREFIX + parameterSuffix; 1098 ParameterEvaluator levelParamEval = null; 1099 ParameterEvaluator consolidationParamEval = null; 1100 1101 //refresh financial object level 1102 acctLine.getObjectCode().refreshReferenceObject("financialObjectLevel"); 1103 1104 levelParamEval = SpringContext.getBean(ParameterService.class).getParameterEvaluator(KfsParameterConstants.PURCHASING_DOCUMENT.class, levelParam, acctLine.getObjectCode().getFinancialObjectLevelCode()); 1105 consolidationParamEval = SpringContext.getBean(ParameterService.class).getParameterEvaluator(KfsParameterConstants.PURCHASING_DOCUMENT.class, consolidationParam, acctLine.getObjectCode().getFinancialObjectLevel().getFinancialConsolidationObjectCode()); 1106 1107 if( (isAllowedFound(levelParamEval) && (isAllowedFound(consolidationParamEval) || isAllowedNotFound(consolidationParamEval) || isDeniedNotFound(consolidationParamEval))) || 1108 (isAllowedNotFound(levelParamEval) && isAllowedFound(consolidationParamEval)) || 1109 (isDeniedFound(levelParamEval) && isAllowedFound(consolidationParamEval)) || 1110 (isDeniedNotFound(levelParamEval) && (isAllowedFound(consolidationParamEval) || isAllowedNotFound(consolidationParamEval) || isDeniedNotFound(consolidationParamEval))) ){ 1111 1112 isObjectCodeTaxable = true; 1113 } 1114 1115 return isObjectCodeTaxable; 1116 } 1117 1118 /** 1119 * Helper method to work with parameter evaluator to find, allowed and found in parameter value. 1120 * 1121 * @param eval 1122 * @return 1123 */ 1124 protected boolean isAllowedFound(ParameterEvaluator eval) { 1125 boolean exists = false; 1126 1127 if (eval.evaluationSucceeds() && eval.constraintIsAllow()) { 1128 exists = true; 1129 } 1130 1131 return exists; 1132 } 1133 1134 /** 1135 * Helper method to work with parameter evaluator to find, allowed and not found in parameter value. 1136 * 1137 * @param eval 1138 * @return 1139 */ 1140 protected boolean isAllowedNotFound(ParameterEvaluator eval) { 1141 boolean exists = false; 1142 1143 if (eval.evaluationSucceeds() == false && eval.constraintIsAllow()) { 1144 exists = true; 1145 } 1146 1147 return exists; 1148 } 1149 1150 /** 1151 * Helper method to work with parameter evaluator to find, denied and found in parameter value. 1152 * 1153 * @param eval 1154 * @return 1155 */ 1156 protected boolean isDeniedFound(ParameterEvaluator eval) { 1157 boolean exists = false; 1158 1159 if (eval.evaluationSucceeds() == false && eval.constraintIsAllow() == false) { 1160 exists = true; 1161 } 1162 1163 return exists; 1164 } 1165 1166 /** 1167 * Helper method to work with parameter evaluator to find, denied and not found in parameter value. 1168 * 1169 * @param eval 1170 * @return 1171 */ 1172 protected boolean isDeniedNotFound(ParameterEvaluator eval) { 1173 boolean exists = false; 1174 1175 if (eval.evaluationSucceeds() && eval.constraintIsAllow() == false) { 1176 exists = true; 1177 } 1178 1179 return exists; 1180 } 1181 1182 /** 1183 * @param useTaxIndicator 1184 * @param deliveryPostalCode 1185 * @param transactionTaxDate 1186 * @param item 1187 * @param itemUseTaxClass 1188 */ 1189 @SuppressWarnings("unchecked") 1190 protected void calculateItemTax(boolean useTaxIndicator, 1191 String deliveryPostalCode, 1192 Date transactionTaxDate, 1193 PurApItem item, 1194 Class itemUseTaxClass, 1195 PurchasingAccountsPayableDocument purapDocument){ 1196 1197 if (!useTaxIndicator){ 1198 if (!StringUtils.equals(item.getItemTypeCode(), PurapConstants.ItemTypeCodes.ITEM_TYPE_PMT_TERMS_DISCOUNT_CODE) && 1199 !StringUtils.equals(item.getItemTypeCode(), PurapConstants.ItemTypeCodes.ITEM_TYPE_ORDER_DISCOUNT_CODE)) { 1200 KualiDecimal taxAmount = taxService.getTotalSalesTaxAmount(transactionTaxDate, deliveryPostalCode, item.getExtendedPrice()); 1201 item.setItemTaxAmount(taxAmount); 1202 } 1203 } else { 1204 KualiDecimal extendedPrice = item.getExtendedPrice(); 1205 1206 if(StringUtils.equals(item.getItemTypeCode(), PurapConstants.ItemTypeCodes.ITEM_TYPE_ORDER_DISCOUNT_CODE)){ 1207 KualiDecimal taxablePrice = getFullDiscountTaxablePrice(extendedPrice, purapDocument); 1208 extendedPrice = taxablePrice; 1209 } 1210 List<TaxDetail> taxDetails = taxService.getUseTaxDetails(transactionTaxDate, deliveryPostalCode, extendedPrice); 1211 List<PurApItemUseTax> newUseTaxItems = new ArrayList<PurApItemUseTax>(); 1212 if (taxDetails != null){ 1213 for (TaxDetail taxDetail : taxDetails) { 1214 try { 1215 PurApItemUseTax useTaxitem = (PurApItemUseTax)itemUseTaxClass.newInstance(); 1216 useTaxitem.setChartOfAccountsCode(taxDetail.getChartOfAccountsCode()); 1217 useTaxitem.setFinancialObjectCode(taxDetail.getFinancialObjectCode()); 1218 useTaxitem.setAccountNumber(taxDetail.getAccountNumber()); 1219 useTaxitem.setItemIdentifier(item.getItemIdentifier()); 1220 useTaxitem.setRateCode(taxDetail.getRateCode()); 1221 useTaxitem.setTaxAmount(taxDetail.getTaxAmount()); 1222 newUseTaxItems.add(useTaxitem); 1223 } 1224 catch (Exception e) { 1225 /** 1226 * Shallow.. This never happen - InstantiationException/IllegalAccessException 1227 * To be safe, throw a runtime exception 1228 */ 1229 throw new RuntimeException(e); 1230 } 1231 } 1232 } 1233 item.setUseTaxItems(newUseTaxItems); 1234 } 1235 } 1236 1237 public KualiDecimal getFullDiscountTaxablePrice(KualiDecimal extendedPrice, PurchasingAccountsPayableDocument purapDocument){ 1238 KualiDecimal taxablePrice = KualiDecimal.ZERO; 1239 KualiDecimal taxableLineItemPrice = KualiDecimal.ZERO; 1240 KualiDecimal totalLineItemPrice = KualiDecimal.ZERO; 1241 boolean useTaxIndicator = purapDocument.isUseTaxIndicator(); 1242 String deliveryState = getDeliveryState(purapDocument); 1243 1244 // iterate over items and calculate tax if taxable 1245 for (PurApItem item : purapDocument.getItems()) { 1246 if (item.getItemType().isLineItemIndicator()){ 1247 //only when extended price exists 1248 if(ObjectUtils.isNotNull(item.getExtendedPrice())){ 1249 if(isTaxable(useTaxIndicator, deliveryState, item)){ 1250 taxableLineItemPrice = taxableLineItemPrice.add(item.getExtendedPrice()); 1251 totalLineItemPrice = totalLineItemPrice.add(item.getExtendedPrice()); 1252 }else{ 1253 totalLineItemPrice = totalLineItemPrice.add(item.getExtendedPrice()); 1254 } 1255 } 1256 } 1257 } 1258 1259 //check nonzero so no divide by zero errors, and make sure extended price is not null 1260 if(totalLineItemPrice.isNonZero() && ObjectUtils.isNotNull(extendedPrice)) 1261 taxablePrice = taxableLineItemPrice.divide(totalLineItemPrice).multiply(extendedPrice); 1262 1263 return taxablePrice; 1264 } 1265 1266 public void prorateForTradeInAndFullOrderDiscount(PurchasingAccountsPayableDocument purDoc) { 1267 1268 if (purDoc instanceof VendorCreditMemoDocument){ 1269 throw new RuntimeException("This method not applicable for VCM documents"); 1270 } 1271 1272 //TODO: are we throwing sufficient errors in this method? 1273 PurApItem fullOrderDiscount = null; 1274 PurApItem tradeIn = null; 1275 KualiDecimal totalAmount = KualiDecimal.ZERO; 1276 KualiDecimal totalTaxAmount = KualiDecimal.ZERO; 1277 1278 List<PurApAccountingLine> distributedAccounts = null; 1279 List<SourceAccountingLine> summaryAccounts = null; 1280 1281 // iterate through below the line and grab FoD and TrdIn. 1282 for (PurApItem item : purDoc.getItems()) { 1283 if (item.getItemTypeCode().equals(PurapConstants.ItemTypeCodes.ITEM_TYPE_ORDER_DISCOUNT_CODE)) { 1284 fullOrderDiscount = item; 1285 } 1286 else if (item.getItemTypeCode().equals(PurapConstants.ItemTypeCodes.ITEM_TYPE_TRADE_IN_CODE)) { 1287 tradeIn = item; 1288 } 1289 } 1290 // If Discount is not null or zero get proration list for all non misc items and set (if not empty?) 1291 if (fullOrderDiscount != null && 1292 fullOrderDiscount.getExtendedPrice() != null && 1293 fullOrderDiscount.getExtendedPrice().isNonZero()) { 1294 1295 // empty 1296 GlobalVariables.getMessageList().add("Full order discount accounts cleared and regenerated"); 1297 fullOrderDiscount.getSourceAccountingLines().clear(); 1298 //total amount is pretax dollars 1299 totalAmount = purDoc.getTotalDollarAmountAboveLineItems().subtract(purDoc.getTotalTaxAmountAboveLineItems()); 1300 totalTaxAmount = purDoc.getTotalTaxAmountAboveLineItems(); 1301 1302 //Before we generate account summary, we should update the account amounts first. 1303 purapAccountingService.updateAccountAmounts(purDoc); 1304 1305 //calculate tax 1306 boolean salesTaxInd = SpringContext.getBean(KualiConfigurationService.class).getIndicatorParameter(PurapConstants.PURAP_NAMESPACE, "Document", PurapParameterConstants.ENABLE_SALES_TAX_IND); 1307 boolean useTaxIndicator = purDoc.isUseTaxIndicator(); 1308 1309 if(salesTaxInd == true && (ObjectUtils.isNull(fullOrderDiscount.getItemTaxAmount()) && useTaxIndicator == false)){ 1310 KualiDecimal discountAmount = fullOrderDiscount.getExtendedPrice(); 1311 KualiDecimal discountTaxAmount = discountAmount.divide(totalAmount).multiply(totalTaxAmount); 1312 1313 fullOrderDiscount.setItemTaxAmount(discountTaxAmount); 1314 } 1315 1316 //generate summary 1317 summaryAccounts = purapAccountingService.generateSummary(PurApItemUtils.getAboveTheLineOnly(purDoc.getItems())); 1318 1319 if (summaryAccounts.size() == 0) { 1320 if (purDoc.shouldGiveErrorForEmptyAccountsProration()) { 1321 GlobalVariables.getMessageMap().putError(PurapConstants.ITEM_TAB_ERROR_PROPERTY, PurapKeyConstants.ERROR_SUMMARY_ACCOUNTS_LIST_EMPTY, "full order discount"); 1322 } 1323 } else { 1324 //prorate accounts 1325 distributedAccounts = purapAccountingService.generateAccountDistributionForProration(summaryAccounts, totalAmount.add(totalTaxAmount), 2, fullOrderDiscount.getAccountingLineClass()); 1326 1327 for (PurApAccountingLine distributedAccount : distributedAccounts) { 1328 BigDecimal percent = distributedAccount.getAccountLinePercent(); 1329 BigDecimal roundedPercent = new BigDecimal(Math.round(percent.doubleValue())); 1330 distributedAccount.setAccountLinePercent(roundedPercent); 1331 } 1332 1333 //update amounts on distributed accounts 1334 purapAccountingService.updateAccountAmountsWithTotal(distributedAccounts, fullOrderDiscount.getTotalAmount()); 1335 1336 fullOrderDiscount.setSourceAccountingLines(distributedAccounts); 1337 } 1338 } else if(fullOrderDiscount != null && 1339 (fullOrderDiscount.getExtendedPrice() == null || fullOrderDiscount.getExtendedPrice().isZero())) { 1340 fullOrderDiscount.getSourceAccountingLines().clear(); 1341 } 1342 1343 // If tradeIn is not null or zero get proration list for all non misc items and set (if not empty?) 1344 if (tradeIn != null && tradeIn.getExtendedPrice() != null && tradeIn.getExtendedPrice().isNonZero()) { 1345 1346 tradeIn.getSourceAccountingLines().clear(); 1347 1348 totalAmount = purDoc.getTotalDollarAmountForTradeIn(); 1349 KualiDecimal tradeInTotalAmount = tradeIn.getTotalAmount(); 1350 //Before we generate account summary, we should update the account amounts first. 1351 purapAccountingService.updateAccountAmounts(purDoc); 1352 1353 //Before generating the summary, lets replace the object code in a cloned accounts collection sothat we can 1354 //consolidate all the modified object codes during summary generation. 1355 List clonedTradeInItems = new ArrayList(); 1356 List objectSubTypesRequiringQty = SpringContext.getBean(KualiConfigurationService.class).getParameterValues(PurapConstants.PURAP_NAMESPACE, "Document", PurapParameterConstants.OBJECT_SUB_TYPES_REQUIRING_QUANTITY); 1357 List purchasingObjectSubTypes = SpringContext.getBean(KualiConfigurationService.class).getParameterValues("KFS-CAB", "Document", PurapParameterConstants.PURCHASING_OBJECT_SUB_TYPES); 1358 1359 String tradeInCapitalObjectCode = SpringContext.getBean(ParameterService.class).getParameterValue(PurapConstants.PURAP_NAMESPACE, "Document", "TRADE_IN_OBJECT_CODE_FOR_CAPITAL_ASSET"); 1360 String tradeInCapitalLeaseObjCd = SpringContext.getBean(ParameterService.class).getParameterValue(PurapConstants.PURAP_NAMESPACE, "Document", "TRADE_IN_OBJECT_CODE_FOR_CAPITAL_LEASE"); 1361 1362 for(PurApItem item : purDoc.getTradeInItems()){ 1363 PurApItem cloneItem = (PurApItem)ObjectUtils.deepCopy(item); 1364 List<PurApAccountingLine> sourceAccountingLines = cloneItem.getSourceAccountingLines(); 1365 for(PurApAccountingLine accountingLine : sourceAccountingLines){ 1366 if(objectSubTypesRequiringQty.contains(accountingLine.getObjectCode().getFinancialObjectSubTypeCode())){ 1367 accountingLine.setFinancialObjectCode(tradeInCapitalObjectCode); 1368 }else if(purchasingObjectSubTypes.contains(accountingLine.getObjectCode().getFinancialObjectSubTypeCode())){ 1369 accountingLine.setFinancialObjectCode(tradeInCapitalLeaseObjCd); 1370 } 1371 } 1372 clonedTradeInItems.add(cloneItem); 1373 } 1374 1375 1376 summaryAccounts = purapAccountingService.generateSummary(clonedTradeInItems); 1377 if (summaryAccounts.size() == 0) { 1378 if (purDoc.shouldGiveErrorForEmptyAccountsProration()) { 1379 GlobalVariables.getMessageMap().putError(PurapConstants.ITEM_TAB_ERROR_PROPERTY, PurapKeyConstants.ERROR_SUMMARY_ACCOUNTS_LIST_EMPTY, "trade in"); 1380 } 1381 } 1382 else { 1383 distributedAccounts = purapAccountingService.generateAccountDistributionForProration(summaryAccounts, totalAmount, 2, tradeIn.getAccountingLineClass()); 1384 for (PurApAccountingLine distributedAccount : distributedAccounts) { 1385 BigDecimal percent = distributedAccount.getAccountLinePercent(); 1386 BigDecimal roundedPercent = new BigDecimal(Math.round(percent.doubleValue())); 1387 distributedAccount.setAccountLinePercent(roundedPercent); 1388 // set the accountAmount same as tradeIn amount not line item's amount 1389 resetAccountAmount(distributedAccount, tradeInTotalAmount); 1390 } 1391 tradeIn.setSourceAccountingLines(distributedAccounts); 1392 } 1393 } 1394 } 1395 1396 private void resetAccountAmount(PurApAccountingLine distributedAccount, KualiDecimal tradeInTotalAmount) { 1397 BigDecimal pct = distributedAccount.getAccountLinePercent(); 1398 BigDecimal amount = tradeInTotalAmount.bigDecimalValue().multiply(pct).divide(new BigDecimal(100)); 1399 distributedAccount.setAmount(new KualiDecimal(amount)); 1400 } 1401 1402 public void clearAllTaxes(PurchasingAccountsPayableDocument purapDoc){ 1403 if (purapDoc.getItems() != null){ 1404 for (int i = 0; i < purapDoc.getItems().size(); i++) { 1405 PurApItem item = purapDoc.getItems().get(i); 1406 if (purapDoc.isUseTaxIndicator()) { 1407 item.setUseTaxItems(new ArrayList<PurApItemUseTax>()); 1408 } 1409 else { 1410 item.setItemTaxAmount(null); 1411 } 1412 } 1413 } 1414 } 1415 1416 /** 1417 * Determines if the item type specified conflict with the Account tax policy. 1418 * 1419 * @param purchasingDocument purchasing document to check 1420 * @param item item to check if in conflict with tax policy 1421 * @return true if item is in conflict, false otherwise 1422 */ 1423 public boolean isItemTypeConflictWithTaxPolicy(PurchasingDocument purchasingDocument, PurApItem item) { 1424 boolean conflict = false; 1425 1426 String deliveryState = getDeliveryState(purchasingDocument); 1427 if (item.getItemType().isLineItemIndicator() ) { 1428 if ( item.getItemType().isTaxableIndicator() ) { 1429 if ( isTaxDisabledForVendor(purchasingDocument)) { 1430 conflict = true; 1431 } 1432 } 1433 // only check account tax policy if accounting line exists 1434 if ( !item.getSourceAccountingLines().isEmpty() ) { 1435 if ( !doesAccountAllowCallToTaxService(deliveryState, item) ) { 1436 conflict = true; 1437 } 1438 } 1439 } 1440 return conflict; 1441 } 1442 1443 /** 1444 * Determines if tax is disabled for vendor, in default always returns false 1445 * @param purapDocument the PurchasingDocument with a vendor to check 1446 * @return true if tax is disabled, false if it is not - in foundation KFS, tax is never disabled 1447 */ 1448 protected boolean isTaxDisabledForVendor( PurchasingDocument purapDocument ) { 1449 return false; 1450 } 1451 1452 public PurapAccountingService getPurapAccountingService() { 1453 return purapAccountingService; 1454 } 1455 1456 public void setPurapAccountingService(PurapAccountingService purapAccountingService) { 1457 this.purapAccountingService = purapAccountingService; 1458 } 1459 } 1460