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.util.ArrayList;
020    import java.util.HashMap;
021    import java.util.Iterator;
022    import java.util.List;
023    import java.util.Map;
024    
025    import org.apache.commons.lang.StringUtils;
026    import org.kuali.kfs.integration.cab.CapitalAssetBuilderModuleService;
027    import org.kuali.kfs.integration.purap.CapitalAssetSystem;
028    import org.kuali.kfs.module.purap.PurapConstants;
029    import org.kuali.kfs.module.purap.PurapPropertyConstants;
030    import org.kuali.kfs.module.purap.PurapRuleConstants;
031    import org.kuali.kfs.module.purap.businessobject.PurApAccountingLine;
032    import org.kuali.kfs.module.purap.businessobject.PurApItem;
033    import org.kuali.kfs.module.purap.businessobject.PurchasingCapitalAssetItem;
034    import org.kuali.kfs.module.purap.businessobject.RequisitionCapitalAssetItem;
035    import org.kuali.kfs.module.purap.businessobject.RequisitionCapitalAssetSystem;
036    import org.kuali.kfs.module.purap.businessobject.RequisitionItem;
037    import org.kuali.kfs.module.purap.document.PurchaseOrderDocument;
038    import org.kuali.kfs.module.purap.document.PurchasingDocument;
039    import org.kuali.kfs.module.purap.document.RequisitionDocument;
040    import org.kuali.kfs.module.purap.document.dataaccess.RequisitionDao;
041    import org.kuali.kfs.module.purap.document.service.PurapService;
042    import org.kuali.kfs.module.purap.document.service.RequisitionService;
043    import org.kuali.kfs.sys.context.SpringContext;
044    import org.kuali.kfs.sys.service.PostalCodeValidationService;
045    import org.kuali.kfs.sys.service.UniversityDateService;
046    import org.kuali.kfs.vnd.businessobject.VendorCommodityCode;
047    import org.kuali.kfs.vnd.businessobject.VendorContract;
048    import org.kuali.kfs.vnd.businessobject.VendorDetail;
049    import org.kuali.kfs.vnd.document.service.VendorService;
050    import org.kuali.rice.kew.exception.WorkflowException;
051    import org.kuali.rice.kim.bo.Person;
052    import org.kuali.rice.kim.service.PersonService;
053    import org.kuali.rice.kns.bo.Note;
054    import org.kuali.rice.kns.service.BusinessObjectService;
055    import org.kuali.rice.kns.service.DateTimeService;
056    import org.kuali.rice.kns.service.DocumentService;
057    import org.kuali.rice.kns.service.KualiConfigurationService;
058    import org.kuali.rice.kns.service.KualiRuleService;
059    import org.kuali.rice.kns.service.ParameterService;
060    import org.kuali.rice.kns.util.KualiDecimal;
061    import org.kuali.rice.kns.util.ObjectUtils;
062    import org.springframework.transaction.annotation.Transactional;
063    
064    
065    
066    /**
067     * Implementation of RequisitionService
068     */
069    @Transactional
070    public class RequisitionServiceImpl implements RequisitionService {
071        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(RequisitionServiceImpl.class);
072    
073        private BusinessObjectService businessObjectService;
074        private CapitalAssetBuilderModuleService capitalAssetBuilderModuleService;
075        private DateTimeService dateTimeService;
076        private DocumentService documentService;
077        private KualiRuleService ruleService;
078        private KualiConfigurationService kualiConfigurationService;
079        private ParameterService parameterService;
080        private PersonService<Person> personService;
081        private PostalCodeValidationService postalCodeValidationService;
082        private PurapService purapService;
083        private RequisitionDao requisitionDao;
084        private UniversityDateService universityDateService;
085        private VendorService vendorService;
086    
087        public PurchasingCapitalAssetItem createCamsItem(PurchasingDocument purDoc, PurApItem purapItem) {
088            PurchasingCapitalAssetItem camsItem = new RequisitionCapitalAssetItem();
089            camsItem.setItemIdentifier(purapItem.getItemIdentifier());
090            // If the system type is INDIVIDUAL then for each of the capital asset items, we need a system attached to it.
091            if (purDoc.getCapitalAssetSystemTypeCode().equals(PurapConstants.CapitalAssetTabStrings.INDIVIDUAL_ASSETS)) {
092                CapitalAssetSystem resultSystem = new RequisitionCapitalAssetSystem();
093                camsItem.setPurchasingCapitalAssetSystem(resultSystem);
094            }
095            camsItem.setPurchasingDocument(purDoc);
096    
097            return camsItem;
098        }
099        
100        public CapitalAssetSystem createCapitalAssetSystem() {
101            CapitalAssetSystem resultSystem = new RequisitionCapitalAssetSystem();
102            return resultSystem;
103        }
104        
105        /**
106         * @see org.kuali.kfs.module.purap.document.service.RequisitionService#getRequisitionById(java.lang.Integer)
107         */
108        public RequisitionDocument getRequisitionById(Integer id) {
109            String documentNumber = requisitionDao.getDocumentNumberForRequisitionId(id);
110            if (ObjectUtils.isNotNull(documentNumber)) {
111                try {
112                    RequisitionDocument doc = (RequisitionDocument) documentService.getByDocumentHeaderId(documentNumber);
113    
114                    return doc;
115                }
116                catch (WorkflowException e) {
117                    String errorMessage = "Error getting requisition document from document service";
118                    LOG.error("getRequisitionById() " + errorMessage, e);
119                    throw new RuntimeException(errorMessage, e);
120                }
121            }
122    
123            return null;
124        }
125    
126        /**
127         * @see org.kuali.kfs.module.purap.document.service.RequisitionService#isAutomaticPurchaseOrderAllowed(org.kuali.kfs.module.purap.document.RequisitionDocument)
128         */
129        public boolean isAutomaticPurchaseOrderAllowed(RequisitionDocument requisition) {
130            LOG.debug("isAutomaticPurchaseOrderAllowed() started");
131    
132            /*
133             * The private checkAutomaticPurchaseOrderRules method contains rules to check if a requisition can become an APO (Automatic
134             * Purchase Order). The method returns a string containing the reason why this method should return false. Save the reason
135             * as a note on the requisition.
136             */
137            String note = checkAutomaticPurchaseOrderRules(requisition);
138            if (StringUtils.isNotEmpty(note)) {
139                note = PurapConstants.REQ_REASON_NOT_APO + note;
140                try {
141                    Note apoNote = documentService.createNoteFromDocument(requisition, note);
142                    documentService.addNoteToDocument(requisition, apoNote);
143                }
144                catch (Exception e) {
145                    throw new RuntimeException(PurapConstants.REQ_UNABLE_TO_CREATE_NOTE + " " + e);
146                }
147    
148                LOG.debug("isAutomaticPurchaseOrderAllowed() return false; " + note);
149                return false;
150            }
151    
152            LOG.debug("isAutomaticPurchaseOrderAllowed() You made it!  Your REQ can become an APO; return true.");
153            return true;
154        }
155    
156        /**
157         * Checks the rule for Automatic Purchase Order eligibility of the requisition and return a String containing the reason why the
158         * requisition was not eligible to become an APO if it was not eligible, or return an empty String if the requisition is
159         * eligible to become an APO
160         * 
161         * @param requisition the requisition document to be checked for APO eligibility.
162         * @return String containing the reason why the requisition was not eligible to become an APO if it was not eligible, or an
163         *         empty String if the requisition is eligible to become an APO.
164         */
165        protected String checkAutomaticPurchaseOrderRules(RequisitionDocument requisition) {
166            String requisitionSource = requisition.getRequisitionSourceCode();
167            KualiDecimal reqTotal = requisition.getTotalDollarAmount();
168            KualiDecimal apoLimit = purapService.getApoLimit(requisition.getVendorContractGeneratedIdentifier(), requisition.getChartOfAccountsCode(), requisition.getOrganizationCode());
169            requisition.setOrganizationAutomaticPurchaseOrderLimit(apoLimit);
170    
171            LOG.debug("isAPO() reqId = " + requisition.getPurapDocumentIdentifier() + "; apoLimit = " + apoLimit + "; reqTotal = " + reqTotal);
172            if (apoLimit == null) {
173                return "APO limit is empty.";
174            }
175            else {
176                if (reqTotal.compareTo(apoLimit) == 1) {
177                    return "Requisition total is greater than the APO limit.";
178                }
179            }
180    
181            if (reqTotal.compareTo(KualiDecimal.ZERO) <= 0) {
182                return "Requisition total is not greater than zero.";
183            }
184    
185            LOG.debug("isAPO() vendor #" + requisition.getVendorHeaderGeneratedIdentifier() + "-" + requisition.getVendorDetailAssignedIdentifier());
186            if (requisition.getVendorHeaderGeneratedIdentifier() == null || requisition.getVendorDetailAssignedIdentifier() == null) {
187                return "Vendor was not selected from the vendor database.";
188            }
189            else {
190                VendorDetail vendorDetail = vendorService.getVendorDetail(requisition.getVendorHeaderGeneratedIdentifier(), requisition.getVendorDetailAssignedIdentifier());
191                if (vendorDetail == null) {
192                    return "Error retrieving vendor from the database.";
193                }
194                if ( StringUtils.isBlank(requisition.getVendorLine1Address()) || 
195                     StringUtils.isBlank(requisition.getVendorCityName()) ||
196                     StringUtils.isBlank(requisition.getVendorCountryCode())) {
197                    return "Requisition does not have all of the vendor address fields that are required for Purchase Order.";
198                }
199                requisition.setVendorRestrictedIndicator(vendorDetail.getVendorRestrictedIndicator());
200                if (requisition.getVendorRestrictedIndicator() != null && requisition.getVendorRestrictedIndicator()) {
201                    return "Selected vendor is marked as restricted.";
202                }
203                requisition.setVendorDetail(vendorDetail);
204    
205                if ((!PurapConstants.RequisitionSources.B2B.equals(requisitionSource)) && ObjectUtils.isNull(requisition.getVendorContractGeneratedIdentifier())) {
206                   Person initiator = getPersonService().getPerson(requisition.getDocumentHeader().getWorkflowDocument().getInitiatorPrincipalId());
207                    VendorContract b2bContract = vendorService.getVendorB2BContract(vendorDetail, initiator.getCampusCode());
208                    if (b2bContract != null) {
209                        return "Standard requisition with no contract selected but a B2B contract exists for the selected vendor.";
210                    }
211                }
212            }
213            
214            //if vendor address isn't complete, no APO
215            if (StringUtils.isBlank(requisition.getVendorLine1Address()) ||
216                    StringUtils.isBlank(requisition.getVendorCityName()) ||
217                    StringUtils.isBlank(requisition.getVendorCountryCode()) ||
218                    !postalCodeValidationService.validateAddress(requisition.getVendorCountryCode(), requisition.getVendorStateCode(), requisition.getVendorPostalCode(), "", "")) {
219                return "Requistion does not contain a complete vendor address";
220            }
221    
222            // These are needed for commodity codes. They are put in here so that
223            // we don't have to loop through items too many times.
224            String purchaseOrderRequiresCommodityCode = parameterService.getParameterValue(PurchaseOrderDocument.class, PurapRuleConstants.ITEMS_REQUIRE_COMMODITY_CODE_IND);
225            boolean commodityCodeRequired = purchaseOrderRequiresCommodityCode.equals("Y");
226            
227            for (Iterator iter = requisition.getItems().iterator(); iter.hasNext();) {
228                RequisitionItem item = (RequisitionItem) iter.next();
229                if (item.isItemRestrictedIndicator()) {
230                    return "Requisition contains an item that is marked as restricted.";
231                }
232                
233                //We only need to check the commodity codes if this is an above the line item.
234                if (item.getItemType().isLineItemIndicator()) {
235                    String commodityCodesReason = "";
236                    List<VendorCommodityCode> vendorCommodityCodes = commodityCodeRequired ? requisition.getVendorDetail().getVendorCommodities() : null;
237                    commodityCodesReason = checkAPORulesPerItemForCommodityCodes(item, vendorCommodityCodes, commodityCodeRequired);
238                    if (StringUtils.isNotBlank(commodityCodesReason)) {
239                        return commodityCodesReason;
240                    }
241                }
242                
243                if (PurapConstants.ItemTypeCodes.ITEM_TYPE_ORDER_DISCOUNT_CODE.equals(item.getItemType().getItemTypeCode()) || PurapConstants.ItemTypeCodes.ITEM_TYPE_TRADE_IN_CODE.equals(item.getItemType().getItemTypeCode())) {
244                    if ((item.getItemUnitPrice() != null) && ((BigDecimal.ZERO.compareTo(item.getItemUnitPrice())) != 0)) {
245                        // discount or trade-in item has unit price that is not empty or zero
246                        return "Requisition contains a " + item.getItemType().getItemTypeDescription() + " item, so it does not qualify as an APO.";
247                    }
248                }
249                
250                if (!PurapConstants.RequisitionSources.B2B.equals(requisitionSource)) {
251                    for (PurApAccountingLine accountingLine : item.getSourceAccountingLines()) {
252                        if (capitalAssetBuilderModuleService.doesAccountingLineFailAutomaticPurchaseOrderRules(accountingLine)) {
253                            return "Requisition contains accounting line with capital object level";
254                        }
255                    }
256                }
257    
258            }// endfor items
259    
260            if (capitalAssetBuilderModuleService.doesDocumentFailAutomaticPurchaseOrderRules(requisition)) {
261                return "Requisition contains capital asset items.";
262            }
263    
264            if (StringUtils.isNotEmpty(requisition.getRecurringPaymentTypeCode())) {
265                return "Payment type is marked as recurring.";
266            }
267    
268            if ((requisition.getPurchaseOrderTotalLimit() != null) && (KualiDecimal.ZERO.compareTo(requisition.getPurchaseOrderTotalLimit()) != 0)) {
269                LOG.debug("isAPO() po total limit is not null and not equal to zero; return false.");
270                return "The 'PO not to exceed' amount has been entered.";
271            }
272    
273            if (StringUtils.isNotEmpty(requisition.getAlternate1VendorName()) || StringUtils.isNotEmpty(requisition.getAlternate2VendorName()) || StringUtils.isNotEmpty(requisition.getAlternate3VendorName()) || StringUtils.isNotEmpty(requisition.getAlternate4VendorName()) || StringUtils.isNotEmpty(requisition.getAlternate5VendorName())) {
274                LOG.debug("isAPO() alternate vendor name exists; return false.");
275                return "Requisition contains additional suggested vendor names.";
276            }
277            
278            if (requisition.isPostingYearNext() && !purapService.isTodayWithinApoAllowedRange()) {
279                return "Requisition is set to encumber next fiscal year and approval is not within APO allowed date range.";
280            }
281    
282            return "";
283        }
284        
285        /**
286         * Checks the APO rules for Commodity Codes. 
287         * The rules are as follow:
288         * 1. If an institution does not require a commodity code on a requisition but 
289         *    does require a commodity code on a purchase order:
290         *    a. If the requisition qualifies for an APO and the commodity code is blank
291         *       on any line item then the system should use the default commodity code
292         *       for the vendor.
293         *    b. If there is not a default commodity code for the vendor then the
294         *       requisition is not eligible to become an APO.
295         * 2. The commodity codes where the restricted indicator is Y should disallow
296         *    the requisition from becoming an APO.
297         * 3. If the commodity code is Inactive when the requisition is finally approved 
298         *    do not allow the requisition to become an APO.
299         *    
300         * @param purItem
301         * @param vendorCommodityCodes
302         * @param commodityCodeRequired
303         * @return
304         */
305        protected String checkAPORulesPerItemForCommodityCodes(RequisitionItem purItem, List<VendorCommodityCode>vendorCommodityCodes, boolean commodityCodeRequired) {
306            // If the commodity code is blank on any line item and a commodity code is required,
307            // then the system should use the default commodity code for the vendor
308            if (purItem.getCommodityCode() == null && commodityCodeRequired) {
309                for (VendorCommodityCode vcc : vendorCommodityCodes) {
310                    if (vcc.isCommodityDefaultIndicator()) {
311                        purItem.setCommodityCode(vcc.getCommodityCode());
312                        purItem.setPurchasingCommodityCode(vcc.getPurchasingCommodityCode());
313                    }
314                }
315            }
316            if (purItem.getCommodityCode() == null) {
317                // If there is not a default commodity code for the vendor then the requisition is not eligible to become an APO.
318                if (commodityCodeRequired)
319                    return "There are missing commodity code(s).";
320            }
321            else if (!purItem.getCommodityCode().isActive()) {
322                return "Requisition contains inactive commodity codes.";
323            }
324            else if (purItem.getCommodityCode().isRestrictedItemsIndicator()) {
325                return "Requisition contains an item with a restricted commodity code.";
326            }
327            return "";
328        }
329        
330        /**
331         * @see org.kuali.kfs.module.purap.document.service.RequisitionService#getRequisitionsAwaitingContractManagerAssignment()
332         */
333        public List<RequisitionDocument> getRequisitionsAwaitingContractManagerAssignment() {
334            Map fieldValues = new HashMap();
335            fieldValues.put(PurapPropertyConstants.STATUS_CODE, PurapConstants.RequisitionStatuses.AWAIT_CONTRACT_MANAGER_ASSGN);
336            List<RequisitionDocument> unassignedRequisitions = new ArrayList(SpringContext.getBean(BusinessObjectService.class).findMatchingOrderBy(RequisitionDocument.class, fieldValues, PurapPropertyConstants.PURAP_DOC_ID, true));
337            return unassignedRequisitions;
338        }
339    
340        /**
341         * @see org.kuali.kfs.module.purap.document.service.RequisitionService#getCountOfRequisitionsAwaitingContractManagerAssignment()
342         */
343        public int getCountOfRequisitionsAwaitingContractManagerAssignment() {
344            List<RequisitionDocument> unassignedRequisitions = getRequisitionsAwaitingContractManagerAssignment();
345            if (ObjectUtils.isNotNull(unassignedRequisitions)) {
346                return unassignedRequisitions.size();
347            }
348            else {
349                return 0;
350            }
351        }
352        
353        public void setBusinessObjectService(BusinessObjectService boService) {
354            this.businessObjectService = boService;
355        }
356    
357        public void setDocumentService(DocumentService documentService) {
358            this.documentService = documentService;
359        }
360    
361        public void setRequisitionDao(RequisitionDao requisitionDao) {
362            this.requisitionDao = requisitionDao;
363        }
364    
365        public void setPurapService(PurapService purapService) {
366            this.purapService = purapService;
367        }
368        
369        public KualiRuleService getRuleService() {
370            return ruleService;
371        }
372    
373        public void setRuleService(KualiRuleService ruleService) {
374            this.ruleService = ruleService;
375        }
376        
377        public void setParameterService(ParameterService parameterService) {
378            this.parameterService = parameterService;
379        }
380    
381        public void setDateTimeService(DateTimeService dateTimeService) {
382            this.dateTimeService = dateTimeService;
383        }
384    
385        public void setUniversityDateService(UniversityDateService universityDateService) {
386            this.universityDateService = universityDateService;
387        }
388    
389        public void setVendorService(VendorService vendorService) {
390            this.vendorService = vendorService;
391        }
392    
393        public void setKualiConfigurationService(KualiConfigurationService kualiConfigurationService) {
394            this.kualiConfigurationService = kualiConfigurationService;
395        }
396    
397        public void setCapitalAssetBuilderModuleService(CapitalAssetBuilderModuleService capitalAssetBuilderModuleService) {
398            this.capitalAssetBuilderModuleService = capitalAssetBuilderModuleService;
399        }
400    
401        public void setPostalCodeValidationService(PostalCodeValidationService postalCodeValidationService) {
402            this.postalCodeValidationService = postalCodeValidationService;
403        }
404    
405        /**
406         * @return Returns the personService.
407         */
408        protected PersonService<Person> getPersonService() {
409            if(personService==null)
410                personService = SpringContext.getBean(PersonService.class);
411            return personService;
412        }
413    
414    }