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.vnd.businessobject.lookup;
017    
018    import java.util.ArrayList;
019    import java.util.Collections;
020    import java.util.HashMap;
021    import java.util.Iterator;
022    import java.util.List;
023    import java.util.Map;
024    import java.util.Properties;
025    
026    import org.apache.commons.lang.StringUtils;
027    import org.kuali.kfs.integration.purap.PurchasingAccountsPayableModuleService;
028    import org.kuali.kfs.sys.KFSConstants;
029    import org.kuali.kfs.sys.context.SpringContext;
030    import org.kuali.kfs.vnd.VendorConstants;
031    import org.kuali.kfs.vnd.VendorKeyConstants;
032    import org.kuali.kfs.vnd.VendorParameterConstants;
033    import org.kuali.kfs.vnd.VendorPropertyConstants;
034    import org.kuali.kfs.vnd.businessobject.VendorAddress;
035    import org.kuali.kfs.vnd.businessobject.VendorDetail;
036    import org.kuali.kfs.vnd.document.service.VendorService;
037    import org.kuali.rice.kns.bo.BusinessObject;
038    import org.kuali.rice.kns.exception.ValidationException;
039    import org.kuali.rice.kns.lookup.AbstractLookupableHelperServiceImpl;
040    import org.kuali.rice.kns.lookup.CollectionIncomplete;
041    import org.kuali.rice.kns.lookup.HtmlData;
042    import org.kuali.rice.kns.lookup.HtmlData.AnchorHtmlData;
043    import org.kuali.rice.kns.service.ParameterService;
044    import org.kuali.rice.kns.util.BeanPropertyComparator;
045    import org.kuali.rice.kns.util.GlobalVariables;
046    import org.kuali.rice.kns.util.KNSConstants;
047    import org.kuali.rice.kns.util.ObjectUtils;
048    import org.kuali.rice.kns.util.UrlFactory;
049    import org.kuali.rice.kns.web.format.Formatter;
050    
051    public class VendorLookupableHelperServiceImpl extends AbstractLookupableHelperServiceImpl {
052        private VendorService vendorService;
053        private ParameterService parameterService;
054    
055        /**
056         * Add custom links to the vendor search results. One to Allow only active parent vendors to create new divisions. Another to
057         * create a link for B2B shopping if PURAP service has been setup to allow for that.
058         * 
059         * @see org.kuali.rice.kns.lookup.LookupableHelperService#getCustomActionUrls(org.kuali.rice.kns.bo.BusinessObject,
060         *      java.util.List, java.util.List pkNames)
061         */
062        @Override
063        public List<HtmlData> getCustomActionUrls(BusinessObject businessObject, List pkNames) {
064            VendorDetail vendor = (VendorDetail) businessObject;
065            List<HtmlData> anchorHtmlDataList = new ArrayList<HtmlData>();
066    
067            AnchorHtmlData anchorHtmlData = super.getUrlData(businessObject, KNSConstants.MAINTENANCE_EDIT_METHOD_TO_CALL, pkNames);
068            anchorHtmlDataList.add(anchorHtmlData);
069            if (vendor.isVendorParentIndicator() && vendor.isActiveIndicator()) {
070                // only allow active parent vendors to create new divisions
071                anchorHtmlDataList.add(super.getUrlData(businessObject, KFSConstants.MAINTENANCE_NEWWITHEXISTING_ACTION, VendorConstants.CREATE_DIVISION, pkNames));
072            }
073            
074            //Adding a "Shopping" link for B2B vendors.
075            String b2bUrlString = SpringContext.getBean(PurchasingAccountsPayableModuleService.class).getB2BUrlString();
076            if (vendor.isB2BVendor() && StringUtils.isNotBlank(b2bUrlString)) {
077                Properties theProperties = new Properties();
078                theProperties.put("channelTitle", "Shop Catalogs");
079                String backLocation = this.getBackLocation();
080                int lastSlash = backLocation.lastIndexOf("/");
081                String returnUrlForShop = backLocation.substring(0, lastSlash+1) + "portal.do";
082                String href = UrlFactory.parameterizeUrl(returnUrlForShop, theProperties);            
083                anchorHtmlDataList.add(new AnchorHtmlData(href + b2bUrlString, null, "shop"));
084            }
085            return anchorHtmlDataList;
086        }
087    
088        /**
089         * Used by getActionUrls to print the url on the Vendor Lookup page for the links to edit a Vendor or to create a new division.
090         * We won't provide a link to copy a vendor because we decided it wouldn't make sense to copy a vendor. We should display the
091         * link to create a new division only if the vendor is a parent vendor, and also remove the vendor detail assigned id from the
092         * query string in the link to create a new division. We'll add the vendor detail assigned id in the query string if the vendor
093         * is not a parent, or if the vendor is a parent and the link is not the create new division link (i.e. if the link is "edit").
094         * We'll always add the vendor header id in the query string in all links.
095         *
096         * @see org.kuali.rice.kns.lookup.AbstractLookupableHelperServiceImpl#getActionUrlHref(org.kuali.rice.kns.bo.BusinessObject, java.lang.String, java.util.List)
097         */
098        @Override
099        protected String getActionUrlHref(BusinessObject businessObject, String methodToCall, List pkNames){
100            if (!methodToCall.equals(KFSConstants.COPY_METHOD)) {
101                Properties parameters = new Properties();
102                parameters.put(KFSConstants.DISPATCH_REQUEST_PARAMETER, methodToCall);
103                parameters.put(KFSConstants.BUSINESS_OBJECT_CLASS_ATTRIBUTE, businessObject.getClass().getName());
104    
105                for (Iterator<String> iter = pkNames.iterator(); iter.hasNext();) {
106                    String fieldNm = iter.next();
107                    if (!fieldNm.equals(VendorPropertyConstants.VENDOR_DETAIL_ASSIGNED_ID) ||
108                            !((VendorDetail) businessObject).isVendorParentIndicator()
109                            || (((VendorDetail) businessObject).isVendorParentIndicator())
110                            && !methodToCall.equals(KFSConstants.MAINTENANCE_NEWWITHEXISTING_ACTION)) {
111                        Object fieldVal = ObjectUtils.getPropertyValue(businessObject, fieldNm);
112                        if (fieldVal == null) {
113                            fieldVal = KFSConstants.EMPTY_STRING;
114                        }
115                        if (fieldVal instanceof java.sql.Date) {
116                            String formattedString = KFSConstants.EMPTY_STRING;
117                            if (Formatter.findFormatter(fieldVal.getClass()) != null) {
118                                Formatter formatter = Formatter.getFormatter(fieldVal.getClass());
119                                formattedString = (String) formatter.format(fieldVal);
120                                fieldVal = formattedString;
121                            }
122                        }
123                        parameters.put(fieldNm, fieldVal.toString());
124                    }
125                }
126                return UrlFactory.parameterizeUrl(KFSConstants.MAINTENANCE_ACTION, parameters);
127            } else {
128                return KFSConstants.EMPTY_STRING;
129            }
130        }
131    
132        /**
133         * Overrides the getSearchResults in the super class so that we can do some customization in our vendor lookup. For example, for
134         * vendor name as the search criteria, we want to search both the vendor detail table and the vendor alias table for the vendor
135         * name. Display the vendor's default address state in the search results.
136         *
137         * @see org.kuali.rice.kns.lookup.Lookupable#getSearchResults(java.util.Map)
138         */
139        @Override
140        public List<BusinessObject> getSearchResults(Map<String, String> fieldValues) {
141            boolean unbounded = false;
142            super.setBackLocation((String) fieldValues.get(KFSConstants.BACK_LOCATION));
143            super.setDocFormKey((String) fieldValues.get(KFSConstants.DOC_FORM_KEY));
144    
145            String vendorName = fieldValues.get(VendorPropertyConstants.VENDOR_NAME);
146    
147            List<BusinessObject> searchResults = (List) getLookupService().findCollectionBySearchHelper(getBusinessObjectClass(), fieldValues, unbounded);
148    
149            // re-run the query against the vendor name alias field if necessary and merge the results
150            // this could double the returned results for the search, but there is no alternative at present
151            // without refactoring of the lookup service
152            if (StringUtils.isNotEmpty(vendorName)) {
153                // if searching by vendorName, also search in list of alias names
154                fieldValues.put(VendorPropertyConstants.VENDOR_ALIAS_NAME_FULL_PATH, vendorName);
155                // also make sure that we only use active aliases to match the query string
156                fieldValues.put(VendorPropertyConstants.VENDOR_ALIAS_ACTIVE, "Y");
157                fieldValues.remove(VendorPropertyConstants.VENDOR_NAME);
158                List<BusinessObject> searchResults2 = (List) getLookupService().findCollectionBySearchHelper(getBusinessObjectClass(), fieldValues, unbounded);
159    
160                searchResults.addAll(searchResults2);
161                if (searchResults instanceof CollectionIncomplete && searchResults2 instanceof CollectionIncomplete) {
162                    ((CollectionIncomplete) searchResults).setActualSizeIfTruncated(((CollectionIncomplete) searchResults).getActualSizeIfTruncated().longValue() + ((CollectionIncomplete) searchResults2).getActualSizeIfTruncated().longValue());
163                }
164            }
165    
166            List<BusinessObject> processedSearchResults = new ArrayList();
167    
168            // loop through results
169            for (BusinessObject businessObject : searchResults) {
170                VendorDetail vendor = (VendorDetail) businessObject;
171    
172                // if its a top level vendor, search for its divisions and add them to the appropriate list then add the vendor to the
173                // return results
174                // if its a division, see if we already have the parent and if not, retrieve it and its divisions then add the parent to
175                // the return results
176    
177    
178                // If this vendor is not already in the processedSearchResults, let's do further processing (e.g. setting the state for
179                // lookup from default address, etc)
180                // and then add it in the processedSearchResults.
181                if (!processedSearchResults.contains(vendor)) {
182                    Map<String, String> tmpValues = new HashMap<String, String>();
183                    List<VendorDetail> relatedVendors = new ArrayList();
184                    tmpValues.put(VendorPropertyConstants.VENDOR_HEADER_GENERATED_ID, vendor.getVendorHeaderGeneratedIdentifier().toString());
185                    relatedVendors = (List) getLookupService().findCollectionBySearchHelper(getBusinessObjectClass(), tmpValues, unbounded);
186    
187                    for (VendorDetail tmpVendor : relatedVendors) {
188                        if (tmpVendor != null && !processedSearchResults.contains(tmpVendor)) {
189                            // populate state from default address
190                            updateDefaultVendorAddress(tmpVendor);
191                            processedSearchResults.add(tmpVendor);
192                        }
193                    }
194    
195                    if (!processedSearchResults.contains(vendor)) {
196                        updateDefaultVendorAddress(vendor);
197                        processedSearchResults.add(vendor);
198                    }
199                }
200            }
201    
202            for (BusinessObject businessObject : processedSearchResults) {
203                VendorDetail vendor = (VendorDetail) businessObject;
204                if (!vendor.isVendorParentIndicator()) {
205                    // find the parent object in the details collection and add that
206                    for (BusinessObject tmpObject : processedSearchResults) {
207                        VendorDetail tmpVendor = (VendorDetail) tmpObject;
208                        if (tmpVendor.getVendorHeaderGeneratedIdentifier().equals(vendor.getVendorHeaderGeneratedIdentifier()) && tmpVendor.isVendorParentIndicator()) {
209                            vendor.setVendorName(tmpVendor.getVendorName() + " > " + vendor.getVendorName());
210                            break;
211                        }
212                    }
213                }
214            }
215    
216            searchResults.clear();
217            searchResults.addAll(processedSearchResults);
218    
219            // sort list if default sort column given
220            List<String> defaultSortColumns = getDefaultSortColumns();
221            if (defaultSortColumns.size() > 0) {
222                Collections.sort(searchResults, new BeanPropertyComparator(getDefaultSortColumns(), true));
223            }
224    
225            return searchResults;
226        }
227    
228        /**
229         * Populates address fields from default address
230         *
231         * @param vendor venodrDetail
232         */
233        private void updateDefaultVendorAddress(VendorDetail vendor) {
234            VendorAddress defaultAddress = vendorService.getVendorDefaultAddress(vendor.getVendorAddresses(), vendor.getVendorHeader().getVendorType().getAddressType().getVendorAddressTypeCode(), "");
235            if (defaultAddress != null ) {
236                if (defaultAddress.getVendorState() != null) {
237                    vendor.setVendorStateForLookup(defaultAddress.getVendorState().getPostalStateName());
238                }
239                vendor.setDefaultAddressLine1(defaultAddress.getVendorLine1Address());
240                vendor.setDefaultAddressLine2(defaultAddress.getVendorLine2Address());
241                vendor.setDefaultAddressCity(defaultAddress.getVendorCityName());
242                vendor.setDefaultAddressPostalCode(defaultAddress.getVendorZipCode());
243                vendor.setDefaultAddressStateCode(defaultAddress.getVendorStateCode());
244                vendor.setDefaultAddressInternationalProvince(defaultAddress.getVendorAddressInternationalProvinceName());
245                vendor.setDefaultAddressCountryCode(defaultAddress.getVendorCountryCode());
246                vendor.setDefaultFaxNumber(defaultAddress.getVendorFaxNumber());
247            }
248        }
249    
250        /**
251         * Overrides a method of the superclass and is now called instead of that one by the Search method of KualiLookupAction when the
252         * Lookupable is of this class. This method first calls the method from the superclass, which should do all the required field
253         * checking, and then goes through all the specific validations which aren't done in at the JSP level. Both the superclass
254         * method and the various validation methods side-effect the adding of errors to the global error map when the input is found to
255         * have an issue.
256         *
257         * @see org.kuali.rice.kns.lookup.AbstractLookupableHelperServiceImpl#validateSearchParameters(java.util.Map)
258         */
259        @Override
260        public void validateSearchParameters(Map fieldValues) {
261            super.validateSearchParameters(fieldValues);
262    
263            validateVendorNumber(fieldValues);
264            validateTaxNumber(fieldValues);
265    
266            if (!GlobalVariables.getMessageMap().isEmpty()) {
267                throw new ValidationException("Error(s) in search criteria");
268            }
269        }
270    
271        /**
272         * Validates that the Vendor Number has no more than one dash in it, and does not consist solely of one dash. Then it calls
273         * extractVendorNumberToVendorIds to obtain vendorHeaderGeneratedId and vendorDetailAssignedId and if either one of the ids
274         * cannot be converted to integers, it will add error that the vendor number must be numerics or numerics separated by a dash.
275         *
276         * @param fieldValues a Map containing only those key-value pairs that have been filled in on the lookup
277         */
278        private void validateVendorNumber(Map fieldValues) {
279            String vendorNumber = (String) fieldValues.get(VendorPropertyConstants.VENDOR_NUMBER);
280            if (StringUtils.isNotBlank(vendorNumber)) {
281                int dashPos1 = vendorNumber.indexOf(VendorConstants.DASH);
282                if (dashPos1 > -1) { // There's a dash in the number.
283                    if (vendorNumber.indexOf(VendorConstants.DASH, dashPos1 + 1) > -1) { // There can't be more than one.
284                        GlobalVariables.getMessageMap().putError(VendorPropertyConstants.VENDOR_NUMBER, VendorKeyConstants.ERROR_VENDOR_LOOKUP_VNDR_NUM_TOO_MANY_DASHES);
285                    }
286                    if (vendorNumber.matches("\\-*")) {
287                        GlobalVariables.getMessageMap().putError(VendorPropertyConstants.VENDOR_NUMBER, VendorKeyConstants.ERROR_VENDOR_LOOKUP_VNDR_NUM_DASHES_ONLY);
288                    }
289                }
290                extractVendorNumberToVendorIds(fieldValues, vendorNumber);
291            }
292        }
293    
294        /**
295         * Parses the vendorNumber string into vendorHeaderGeneratedIdentifier and vendorDetailAssignedIdentifier, validates that both
296         * fields would be able to be converted into integers, if so it will add both fields into the search criterias map in the
297         * fieldValues and remove the vendorNumber from the fieldValues. If the two fields cannot be converted into integers, this
298         * method will add error message to the errorMap in GlobalVariables that the vendor number must be numeric or numerics separated
299         * by a dash.
300         *
301         * @param fieldValues a Map containing only those key-value pairs that have been filled in on the lookup
302         * @param vendorNumber vendor number String
303         */
304        private void extractVendorNumberToVendorIds(Map fieldValues, String vendorNumber) {
305            String vendorHeaderGeneratedIdentifier = null;
306            String vendorDetailAssignedIdentifier = null;
307            int indexOfDash = vendorNumber.indexOf(VendorConstants.DASH);
308            if (indexOfDash < 0) {
309                vendorHeaderGeneratedIdentifier = vendorNumber;
310            }
311            else {
312                vendorHeaderGeneratedIdentifier = vendorNumber.substring(0, indexOfDash);
313                vendorDetailAssignedIdentifier = vendorNumber.substring(indexOfDash + 1, vendorNumber.length());
314            }
315            try {
316                if (StringUtils.isNotEmpty(vendorHeaderGeneratedIdentifier)) {
317                    Integer.parseInt(vendorHeaderGeneratedIdentifier);
318                }
319                if (StringUtils.isNotEmpty(vendorDetailAssignedIdentifier)) {
320                    Integer.parseInt(vendorDetailAssignedIdentifier);
321                }
322                fieldValues.remove(VendorPropertyConstants.VENDOR_NUMBER);
323                fieldValues.put(VendorPropertyConstants.VENDOR_HEADER_GENERATED_ID, vendorHeaderGeneratedIdentifier);
324                fieldValues.put(VendorPropertyConstants.VENDOR_DETAIL_ASSIGNED_ID, vendorDetailAssignedIdentifier);
325            }
326            catch (NumberFormatException headerExc) {
327                GlobalVariables.getMessageMap().putError(VendorPropertyConstants.VENDOR_NUMBER, VendorKeyConstants.ERROR_VENDOR_LOOKUP_VNDR_NUM_NUMERIC_DASH_SEPARATED);
328            }
329        }
330    
331        /**
332         * Validates that the tax number is 9 digits long.
333         *
334         * @param fieldValues a Map containing only those key-value pairs that have been filled in on the lookup
335         */
336        private void validateTaxNumber(Map fieldValues) {
337            String taxNumber = (String) fieldValues.get(VendorPropertyConstants.VENDOR_TAX_NUMBER);
338            if (StringUtils.isNotBlank(taxNumber) && (!StringUtils.isNumeric(taxNumber) || taxNumber.length() != 9)) {
339                GlobalVariables.getMessageMap().putError(VendorPropertyConstants.VENDOR_TAX_NUMBER, VendorKeyConstants.ERROR_VENDOR_LOOKUP_TAX_NUM_INVALID);
340            }
341        }
342    
343        public void setVendorService(VendorService vendorService) {
344            this.vendorService = vendorService;
345        }
346    
347        public void setParameterService(ParameterService parameterService) {
348            this.parameterService = parameterService;
349        }
350    
351    }