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