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