| /******************************************************************************* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| *******************************************************************************/ |
| package org.ofbiz.order.shoppingcart; |
| |
| import java.math.BigDecimal; |
| import java.sql.Timestamp; |
| import java.text.NumberFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| import javolution.util.FastMap; |
| |
| import org.ofbiz.base.util.Debug; |
| import org.ofbiz.base.util.GeneralException; |
| import org.ofbiz.base.util.ObjectType; |
| import org.ofbiz.base.util.UtilHttp; |
| import org.ofbiz.base.util.UtilMisc; |
| import org.ofbiz.base.util.UtilNumber; |
| import org.ofbiz.base.util.UtilProperties; |
| import org.ofbiz.base.util.UtilValidate; |
| import org.ofbiz.entity.Delegator; |
| import org.ofbiz.entity.GenericEntityException; |
| import org.ofbiz.entity.GenericValue; |
| import org.ofbiz.entity.util.EntityTypeUtil; |
| import org.ofbiz.entity.util.EntityUtil; |
| import org.ofbiz.order.order.OrderReadHelper; |
| import org.ofbiz.order.shoppingcart.product.ProductPromoWorker; |
| import org.ofbiz.product.config.ProductConfigWorker; |
| import org.ofbiz.product.config.ProductConfigWrapper; |
| import org.ofbiz.product.product.ProductWorker; |
| import org.ofbiz.security.Security; |
| import org.ofbiz.service.LocalDispatcher; |
| import org.ofbiz.service.ModelService; |
| import org.ofbiz.service.ServiceUtil; |
| |
| /** |
| * A facade over the |
| * {@link org.ofbiz.order.shoppingcart.ShoppingCart ShoppingCart} |
| * providing catalog and product services to simplify the interaction |
| * with the cart directly. |
| */ |
| public class ShoppingCartHelper { |
| |
| public static final String resource = "OrderUiLabels"; |
| public static String module = ShoppingCartHelper.class.getName(); |
| public static final String resource_error = "OrderErrorUiLabels"; |
| |
| // The shopping cart to manipulate |
| private ShoppingCart cart = null; |
| |
| // The entity engine delegator |
| private Delegator delegator = null; |
| |
| // The service invoker |
| private LocalDispatcher dispatcher = null; |
| |
| /** |
| * Changes will be made to the cart directly, as opposed |
| * to a copy of the cart provided. |
| * |
| * @param cart The cart to manipulate |
| */ |
| public ShoppingCartHelper(Delegator delegator, LocalDispatcher dispatcher, ShoppingCart cart) { |
| this.dispatcher = dispatcher; |
| this.delegator = delegator; |
| this.cart = cart; |
| |
| if (delegator == null) { |
| this.delegator = dispatcher.getDelegator(); |
| } |
| if (dispatcher == null) { |
| throw new IllegalArgumentException("Dispatcher argument is null"); |
| } |
| if (cart == null) { |
| throw new IllegalArgumentException("ShoppingCart argument is null"); |
| } |
| } |
| |
| /** Event to add an item to the shopping cart. */ |
| public Map<String, Object> addToCart(String catalogId, String shoppingListId, String shoppingListItemSeqId, String productId, |
| String productCategoryId, String itemType, String itemDescription, |
| BigDecimal price, BigDecimal amount, BigDecimal quantity, |
| java.sql.Timestamp reservStart, BigDecimal reservLength, BigDecimal reservPersons, |
| java.sql.Timestamp shipBeforeDate, java.sql.Timestamp shipAfterDate, |
| ProductConfigWrapper configWrapper, String itemGroupNumber, Map<String, ? extends Object> context, String parentProductId) { |
| |
| return addToCart(catalogId,shoppingListId,shoppingListItemSeqId,productId, |
| productCategoryId,itemType,itemDescription,price,amount,quantity, |
| reservStart,reservLength,reservPersons,null,null,shipBeforeDate,shipAfterDate, |
| configWrapper,itemGroupNumber,context,parentProductId); |
| } |
| |
| /** Event to add an item to the shopping cart with accommodation. */ |
| public Map<String, Object> addToCart(String catalogId, String shoppingListId, String shoppingListItemSeqId, String productId, |
| String productCategoryId, String itemType, String itemDescription, |
| BigDecimal price, BigDecimal amount, BigDecimal quantity, |
| java.sql.Timestamp reservStart, BigDecimal reservLength, BigDecimal reservPersons, String accommodationMapId,String accommodationSpotId, |
| java.sql.Timestamp shipBeforeDate, java.sql.Timestamp shipAfterDate, |
| ProductConfigWrapper configWrapper, String itemGroupNumber, Map<String, ? extends Object> context, String parentProductId) { |
| Map<String, Object> result = null; |
| Map<String, Object> attributes = null; |
| String pProductId = null; |
| pProductId = parentProductId; |
| // price sanity check |
| if (productId == null && price != null && price.compareTo(BigDecimal.ZERO) < 0) { |
| String errMsg = UtilProperties.getMessage(resource_error, "cart.price_not_positive_number", this.cart.getLocale()); |
| result = ServiceUtil.returnError(errMsg); |
| return result; |
| } |
| |
| // quantity sanity check |
| if (quantity.compareTo(BigDecimal.ONE) < 0) { |
| String errMsg = UtilProperties.getMessage(resource_error, "cart.quantity_not_positive_number", this.cart.getLocale()); |
| result = ServiceUtil.returnError(errMsg); |
| return result; |
| } |
| |
| // amount sanity check |
| if (amount != null && amount.compareTo(BigDecimal.ZERO) < 0) { |
| String errMsg = UtilProperties.getMessage(resource_error, "cart.amount_not_positive_number", this.cart.getLocale()); |
| result = ServiceUtil.returnError(errMsg); |
| return result; |
| } |
| |
| // check desiredDeliveryDate syntax and remove if empty |
| String ddDate = (String) context.get("itemDesiredDeliveryDate"); |
| if (!UtilValidate.isEmpty(ddDate)) { |
| try { |
| java.sql.Timestamp.valueOf((String) context.get("itemDesiredDeliveryDate")); |
| } catch (IllegalArgumentException e) { |
| return ServiceUtil.returnError(UtilProperties.getMessage(resource_error,"OrderInvalidDesiredDeliveryDateSyntaxError",this.cart.getLocale())); |
| } |
| } else { |
| context.remove("itemDesiredDeliveryDate"); |
| } |
| |
| // remove an empty comment |
| String comment = (String) context.get("itemComment"); |
| if (UtilValidate.isEmpty(comment)) { |
| context.remove("itemComment"); |
| } |
| |
| // stores the default desired delivery date in the cart if need |
| if (!UtilValidate.isEmpty(context.get("useAsDefaultDesiredDeliveryDate"))) { |
| cart.setDefaultItemDeliveryDate((String) context.get("itemDesiredDeliveryDate")); |
| } else { |
| // do we really want to clear this if it isn't checked? |
| cart.setDefaultItemDeliveryDate(null); |
| } |
| |
| // stores the default comment in session if need |
| if (!UtilValidate.isEmpty(context.get("useAsDefaultComment"))) { |
| cart.setDefaultItemComment((String) context.get("itemComment")); |
| } else { |
| // do we really want to clear this if it isn't checked? |
| cart.setDefaultItemComment(null); |
| } |
| |
| // Create a HashMap of product attributes - From ShoppingCartItem.attributeNames[] |
| for (int namesIdx = 0; namesIdx < ShoppingCartItem.attributeNames.length; namesIdx++) { |
| if (attributes == null) |
| attributes = new HashMap<String, Object>(); |
| if (context.containsKey(ShoppingCartItem.attributeNames[namesIdx])) { |
| attributes.put(ShoppingCartItem.attributeNames[namesIdx], context.get(ShoppingCartItem.attributeNames[namesIdx])); |
| } |
| } |
| |
| // check for required amount flag; if amount and no flag set to 0 |
| GenericValue product = null; |
| if (productId != null) { |
| try { |
| product = delegator.findByPrimaryKeyCache("Product", UtilMisc.toMap("productId", productId)); |
| } catch (GenericEntityException e) { |
| Debug.logError(e, "Unable to lookup product : " + productId, module); |
| } |
| if (product == null || product.get("requireAmount") == null || "N".equals(product.getString("requireAmount"))) { |
| amount = null; |
| } |
| Debug.logInfo("carthelper productid " + productId,module); |
| Debug.logInfo("parent productid " + pProductId,module); |
| //if (product != null && !"Y".equals(product.getString("isVariant"))) |
| // pProductId = null; |
| |
| } |
| |
| // Get the additional features selected for the product (if any) |
| Map<String, Object> selectedFeatures = UtilHttp.makeParamMapWithPrefix(context, null, "FT", null); |
| Iterator<String> selectedFeaturesTypes = selectedFeatures.keySet().iterator(); |
| Map<String, GenericValue> additionalFeaturesMap = FastMap.newInstance(); |
| while (selectedFeaturesTypes.hasNext()) { |
| String selectedFeatureType = selectedFeaturesTypes.next(); |
| String selectedFeatureValue = (String)selectedFeatures.get(selectedFeatureType); |
| if (UtilValidate.isNotEmpty(selectedFeatureValue)) { |
| GenericValue productFeatureAndAppl = null; |
| try { |
| productFeatureAndAppl = EntityUtil.getFirst(EntityUtil.filterByDate(delegator.findByAnd("ProductFeatureAndAppl", |
| UtilMisc.toMap("productId", productId, |
| "productFeatureTypeId", selectedFeatureType, |
| "productFeatureId", selectedFeatureValue)))); |
| } catch (GenericEntityException gee) { |
| Debug.logError(gee, module); |
| } |
| if (UtilValidate.isNotEmpty(productFeatureAndAppl)) { |
| productFeatureAndAppl.set("productFeatureApplTypeId", "STANDARD_FEATURE"); |
| } |
| additionalFeaturesMap.put(selectedFeatureType, productFeatureAndAppl); |
| } |
| } |
| |
| // get order item attributes |
| Map<String, String> orderItemAttributes = FastMap.newInstance(); |
| String orderItemAttributePrefix = UtilProperties.getPropertyValue("order.properties", "order.item.attr.prefix"); |
| for (Entry<String, ? extends Object> entry : context.entrySet()) { |
| if (entry.getKey().toString().contains(orderItemAttributePrefix) && UtilValidate.isNotEmpty(entry.getValue())) { |
| orderItemAttributes.put(entry.getKey().replaceAll(orderItemAttributePrefix, ""), entry.getValue().toString()); |
| } |
| } |
| |
| // add or increase the item to the cart |
| int itemId = -1; |
| try { |
| if (productId != null) { |
| |
| itemId = cart.addOrIncreaseItem(productId, amount, quantity, reservStart, reservLength, |
| reservPersons, accommodationMapId, accommodationSpotId, shipBeforeDate, shipAfterDate, additionalFeaturesMap, attributes, |
| orderItemAttributes, catalogId, configWrapper, itemType, itemGroupNumber, pProductId, dispatcher); |
| |
| } else { |
| itemId = cart.addNonProductItem(itemType, itemDescription, productCategoryId, price, quantity, attributes, catalogId, itemGroupNumber, dispatcher); |
| } |
| |
| // set the shopping list info |
| if (itemId > -1 && shoppingListId != null && shoppingListItemSeqId != null) { |
| ShoppingCartItem item = cart.findCartItem(itemId); |
| item.setShoppingList(shoppingListId, shoppingListItemSeqId); |
| } |
| } catch (CartItemModifyException e) { |
| if (cart.getOrderType().equals("PURCHASE_ORDER")) { |
| String errMsg = UtilProperties.getMessage(resource_error, "cart.product_not_valid_for_supplier", this.cart.getLocale()); |
| errMsg = errMsg + " (" + e.getMessage() + ")"; |
| result = ServiceUtil.returnError(errMsg); |
| } else { |
| result = ServiceUtil.returnError(e.getMessage()); |
| } |
| return result; |
| } catch (ItemNotFoundException e) { |
| result = ServiceUtil.returnError(e.getMessage()); |
| return result; |
| } |
| |
| // Indicate there were no critical errors |
| result = ServiceUtil.returnSuccess(); |
| if (itemId != -1) { |
| result.put("itemId", new Integer(itemId)); |
| } |
| return result; |
| } |
| |
| public Map<String, Object> addToCartFromOrder(String catalogId, String orderId, String[] itemIds, boolean addAll, String itemGroupNumber) { |
| ArrayList<String> errorMsgs = new ArrayList<String>(); |
| Map<String, Object> result; |
| String errMsg = null; |
| |
| if (orderId == null || orderId.length() <= 0) { |
| errMsg = UtilProperties.getMessage(resource_error,"cart.order_not_specified_to_add_from", this.cart.getLocale()); |
| result = ServiceUtil.returnError(errMsg); |
| return result; |
| } |
| |
| boolean noItems = true; |
| List<? extends Object> itemIdList = null; |
| Iterator<? extends Object> itemIter = null; |
| OrderReadHelper orderHelper = new OrderReadHelper(delegator, orderId); |
| if (addAll) { |
| itemIdList = orderHelper.getOrderItems(); |
| } else { |
| if (itemIds != null) { |
| itemIdList = Arrays.asList(itemIds); |
| } |
| } |
| if (UtilValidate.isNotEmpty(itemIdList)) { |
| itemIter = itemIdList.iterator(); |
| } |
| |
| String orderItemTypeId = null; |
| String productId = null; |
| if (itemIter != null && itemIter.hasNext()) { |
| while (itemIter.hasNext()) { |
| GenericValue orderItem = null; |
| Object value = itemIter.next(); |
| if (value instanceof GenericValue) { |
| orderItem = (GenericValue) value; |
| } else { |
| String orderItemSeqId = (String) value; |
| orderItem = orderHelper.getOrderItem(orderItemSeqId); |
| } |
| orderItemTypeId = orderItem.getString("orderItemTypeId"); |
| productId = orderItem.getString("productId"); |
| // do not store rental items |
| if (orderItemTypeId.equals("RENTAL_ORDER_ITEM")) |
| continue; |
| // never read: int itemId = -1; |
| if (UtilValidate.isNotEmpty(productId) && orderItem.get("quantity") != null) { |
| BigDecimal amount = orderItem.getBigDecimal("selectedAmount"); |
| ProductConfigWrapper configWrapper = null; |
| String aggregatedProdId = null; |
| if (EntityTypeUtil.hasParentType(delegator, "ProductType", "productTypeId", ProductWorker.getProductTypeId(delegator, productId), "parentTypeId", "AGGREGATED")) { |
| try { |
| GenericValue instanceProduct = delegator.findByPrimaryKey("Product", UtilMisc.toMap("productId", productId)); |
| String configId = instanceProduct.getString("configId"); |
| aggregatedProdId = ProductWorker.getInstanceAggregatedId(delegator, productId); |
| configWrapper = ProductConfigWorker.loadProductConfigWrapper(delegator, dispatcher, configId, aggregatedProdId, cart.getProductStoreId(), catalogId, cart.getWebSiteId(), cart.getCurrency(), cart.getLocale(), cart.getAutoUserLogin()); |
| } catch (GenericEntityException e) { |
| errorMsgs.add(e.getMessage()); |
| } |
| |
| } |
| try { |
| this.cart.addOrIncreaseItem(UtilValidate.isNotEmpty(aggregatedProdId) ? aggregatedProdId : productId, amount, orderItem.getBigDecimal("quantity"), |
| null, null, null, null, null, null, null, catalogId, configWrapper, orderItemTypeId, itemGroupNumber, null, dispatcher); |
| noItems = false; |
| } catch (CartItemModifyException e) { |
| errorMsgs.add(e.getMessage()); |
| } catch (ItemNotFoundException e) { |
| errorMsgs.add(e.getMessage()); |
| } |
| } |
| } |
| if (errorMsgs.size() > 0) { |
| result = ServiceUtil.returnError(errorMsgs); |
| result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_SUCCESS); |
| return result; // don't return error because this is a non-critical error and should go back to the same page |
| } |
| } else { |
| noItems = true; |
| } |
| |
| if (noItems) { |
| result = ServiceUtil.returnSuccess(); |
| result.put("_ERROR_MESSAGE_", UtilProperties.getMessage(resource_error,"OrderNoItemsFoundToAdd", this.cart.getLocale())); |
| return result; // don't return error because this is a non-critical error and should go back to the same page |
| } |
| |
| result = ServiceUtil.returnSuccess(); |
| return result; |
| } |
| |
| /** |
| * Adds all products in a category according to quantity request parameter |
| * for each; if no parameter for a certain product in the category, or if |
| * quantity is 0, do not add. |
| * If a _ign_${itemGroupNumber} is appended to the name it will be put in that group instead of the default in the request parameter in itemGroupNumber |
| * |
| * There are 2 options for the syntax: |
| * - name="quantity_${productId}" value="${quantity} |
| * - name="product_${whatever}" value="${productId}" (note: quantity is always 1) |
| */ |
| public Map<String, Object> addToCartBulk(String catalogId, String categoryId, Map<String, ? extends Object> context) { |
| String itemGroupNumber = (String) context.get("itemGroupNumber"); |
| // use this prefix for the main structure such as a checkbox or a text input where name="quantity_${productId}" value="${quantity}" |
| String keyPrefix = "quantity_"; |
| // use this prefix for a different structure, useful for radio buttons; can have any suffix, name="product_${whatever}" value="${productId}" and quantity is always 1 |
| String productQuantityKeyPrefix = "product_"; |
| |
| // If a _ign_${itemGroupNumber} is appended to the name it will be put in that group instead of the default in the request parameter in itemGroupNumber |
| String ignSeparator = "_ign_"; |
| |
| // iterate through the context and find all keys that start with "quantity_" |
| for (Map.Entry<String, ? extends Object> entry : context.entrySet()) { |
| String productId = null; |
| String quantStr = null; |
| String itemGroupNumberToUse = itemGroupNumber; |
| String originalProductId = null; |
| if (entry.getKey() instanceof String) { |
| String key = entry.getKey(); |
| //Debug.logInfo("Bulk Key: " + key, module); |
| |
| int ignIndex = key.indexOf(ignSeparator); |
| if (ignIndex > 0) { |
| itemGroupNumberToUse = key.substring(ignIndex + ignSeparator.length()); |
| key = key.substring(0, ignIndex); |
| } |
| |
| if (key.startsWith(keyPrefix)) { |
| productId = key.substring(keyPrefix.length()); |
| quantStr = (String) entry.getValue(); |
| } else if (key.startsWith(productQuantityKeyPrefix)) { |
| productId = (String) entry.getValue(); |
| quantStr = "1"; |
| } else { |
| continue; |
| } |
| } else { |
| continue; |
| } |
| |
| if (UtilValidate.isNotEmpty(quantStr)) { |
| BigDecimal quantity = BigDecimal.ZERO; |
| |
| try { |
| quantity = new BigDecimal(quantStr); |
| } catch (NumberFormatException nfe) { |
| quantity = BigDecimal.ZERO; |
| } |
| if (quantity.compareTo(BigDecimal.ZERO) > 0) { |
| // check for alternative packing |
| if(ProductWorker.isAlternativePacking(delegator, null , productId)){ |
| GenericValue originalProduct = null; |
| originalProductId = productId; |
| productId = ProductWorker.getOriginalProductId(delegator, productId); |
| try { |
| originalProduct = delegator.findByPrimaryKey("Product", UtilMisc.toMap("productId", originalProductId)); |
| } catch (GenericEntityException e) { |
| Debug.logError(e, "Error getting parent product", module); |
| } |
| BigDecimal piecesIncluded = BigDecimal.ZERO; |
| if(originalProduct != null){ |
| piecesIncluded = new BigDecimal(originalProduct.getLong("piecesIncluded")); |
| quantity = quantity.multiply(piecesIncluded); |
| } |
| } |
| |
| try { |
| if (Debug.verboseOn()) Debug.logVerbose("Bulk Adding to cart [" + quantity + "] of [" + productId + "] in Item Group [" + itemGroupNumber + "]", module); |
| this.cart.addOrIncreaseItem(productId, null, quantity, null, null, null, null, null, null, null, catalogId, null, null, itemGroupNumberToUse, originalProductId, dispatcher); |
| } catch (CartItemModifyException e) { |
| return ServiceUtil.returnError(e.getMessage()); |
| } catch (ItemNotFoundException e) { |
| return ServiceUtil.returnError(e.getMessage()); |
| } |
| } |
| } |
| } |
| |
| //Indicate there were no non critical errors |
| return ServiceUtil.returnSuccess(); |
| } |
| |
| /** |
| * Adds a set of requirements to the cart. |
| */ |
| public Map<String, Object> addToCartBulkRequirements(String catalogId, Map<String, ? extends Object> context) { |
| String itemGroupNumber = (String) context.get("itemGroupNumber"); |
| // check if we are using per row submit |
| boolean useRowSubmit = (!context.containsKey("_useRowSubmit"))? false : |
| "Y".equalsIgnoreCase((String)context.get("_useRowSubmit")); |
| |
| // check if we are to also look in a global scope (no delimiter) |
| //boolean checkGlobalScope = (!context.containsKey("_checkGlobalScope"))? false : |
| // "Y".equalsIgnoreCase((String)context.get("_checkGlobalScope")); |
| |
| // The number of multi form rows is retrieved |
| int rowCount = UtilHttp.getMultiFormRowCount(context); |
| |
| // assume that the facility is the same for all requirements |
| String facilityId = (String) context.get("facilityId_o_0"); |
| if (UtilValidate.isNotEmpty(facilityId)) { |
| cart.setFacilityId(facilityId); |
| } |
| |
| // now loop throw the rows and prepare/invoke the service for each |
| for (int i = 0; i < rowCount; i++) { |
| String productId = null; |
| String quantStr = null; |
| String requirementId = null; |
| String thisSuffix = UtilHttp.MULTI_ROW_DELIMITER + i; |
| boolean rowSelected = (!context.containsKey("_rowSubmit" + thisSuffix))? false : |
| "Y".equalsIgnoreCase((String)context.get("_rowSubmit" + thisSuffix)); |
| |
| // make sure we are to process this row |
| if (useRowSubmit && !rowSelected) { |
| continue; |
| } |
| |
| // build the context |
| if (context.containsKey("productId" + thisSuffix)) { |
| productId = (String) context.get("productId" + thisSuffix); |
| quantStr = (String) context.get("quantity" + thisSuffix); |
| requirementId = (String) context.get("requirementId" + thisSuffix); |
| GenericValue requirement = null; |
| try { |
| requirement = delegator.findByPrimaryKey("Requirement", UtilMisc.toMap("requirementId", requirementId)); |
| } catch (GenericEntityException gee) { |
| } |
| if (requirement == null) { |
| return ServiceUtil.returnFailure(UtilProperties.getMessage(resource, |
| "OrderRequirementDoesNotExists", |
| UtilMisc.toMap("requirementId", requirementId), cart.getLocale())); |
| } |
| |
| if (UtilValidate.isNotEmpty(quantStr)) { |
| BigDecimal quantity = BigDecimal.ZERO; |
| try { |
| quantity = (BigDecimal) ObjectType.simpleTypeConvert(quantStr, "BigDecimal", null, cart.getLocale()); |
| } catch (GeneralException ge) { |
| quantity = BigDecimal.ZERO; |
| } |
| if (quantity.compareTo(BigDecimal.ZERO) > 0) { |
| Iterator<ShoppingCartItem> items = this.cart.iterator(); |
| boolean requirementAlreadyInCart = false; |
| while (items.hasNext() && !requirementAlreadyInCart) { |
| ShoppingCartItem sci = items.next(); |
| if (sci.getRequirementId() != null && sci.getRequirementId().equals(requirementId)) { |
| requirementAlreadyInCart = true; |
| continue; |
| } |
| } |
| if (requirementAlreadyInCart) { |
| if (Debug.warningOn()) Debug.logWarning(UtilProperties.getMessage(resource_error, "OrderTheRequirementIsAlreadyInTheCartNotAdding", UtilMisc.toMap("requirementId",requirementId), cart.getLocale()), module); |
| continue; |
| } |
| try { |
| if (Debug.verboseOn()) Debug.logVerbose("Bulk Adding to cart requirement [" + quantity + "] of [" + productId + "]", module); |
| int index = this.cart.addOrIncreaseItem(productId, null, quantity, null, null, null, requirement.getTimestamp("requiredByDate"), null, null, null, catalogId, null, null, itemGroupNumber, null, dispatcher); |
| ShoppingCartItem sci = this.cart.items().get(index); |
| sci.setRequirementId(requirementId); |
| } catch (CartItemModifyException e) { |
| return ServiceUtil.returnError(e.getMessage()); |
| } catch (ItemNotFoundException e) { |
| return ServiceUtil.returnError(e.getMessage()); |
| } |
| } |
| } |
| } |
| } |
| //Indicate there were no non critical errors |
| return ServiceUtil.returnSuccess(); |
| } |
| |
| /** |
| * Adds all products in a category according to default quantity on ProductCategoryMember |
| * for each; if no default for a certain product in the category, or if |
| * quantity is 0, do not add |
| */ |
| public Map<String, Object> addCategoryDefaults(String catalogId, String categoryId, String itemGroupNumber) { |
| ArrayList<String> errorMsgs = new ArrayList<String>(); |
| Map<String, Object> result = null; |
| String errMsg = null; |
| |
| if (categoryId == null || categoryId.length() <= 0) { |
| errMsg = UtilProperties.getMessage(resource_error,"cart.category_not_specified_to_add_from", this.cart.getLocale()); |
| result = ServiceUtil.returnError(errMsg); |
| return result; |
| } |
| |
| Collection<GenericValue> prodCatMemberCol = null; |
| |
| try { |
| prodCatMemberCol = delegator.findByAndCache("ProductCategoryMember", UtilMisc.toMap("productCategoryId", categoryId)); |
| } catch (GenericEntityException e) { |
| Debug.logWarning(e.toString(), module); |
| Map<String, Object> messageMap = UtilMisc.<String, Object>toMap("categoryId", categoryId); |
| messageMap.put("message", e.getMessage()); |
| errMsg = UtilProperties.getMessage(resource_error,"cart.could_not_get_products_in_category_cart", messageMap, this.cart.getLocale()); |
| result = ServiceUtil.returnError(errMsg); |
| return result; |
| } |
| |
| if (prodCatMemberCol == null) { |
| Map<String, Object> messageMap = UtilMisc.<String, Object>toMap("categoryId", categoryId); |
| errMsg = UtilProperties.getMessage(resource_error,"cart.could_not_get_products_in_category", messageMap, this.cart.getLocale()); |
| result = ServiceUtil.returnError(errMsg); |
| return result; |
| } |
| |
| BigDecimal totalQuantity = BigDecimal.ZERO; |
| Iterator<GenericValue> pcmIter = prodCatMemberCol.iterator(); |
| |
| while (pcmIter.hasNext()) { |
| GenericValue productCategoryMember = pcmIter.next(); |
| BigDecimal quantity = productCategoryMember.getBigDecimal("quantity"); |
| |
| if (quantity != null && quantity.compareTo(BigDecimal.ZERO) > 0) { |
| try { |
| this.cart.addOrIncreaseItem(productCategoryMember.getString("productId"), |
| null, quantity, null, null, null, null, null, null, null, |
| catalogId, null, null, itemGroupNumber, null, dispatcher); |
| totalQuantity = totalQuantity.add(quantity); |
| } catch (CartItemModifyException e) { |
| errorMsgs.add(e.getMessage()); |
| } catch (ItemNotFoundException e) { |
| errorMsgs.add(e.getMessage()); |
| } |
| } |
| } |
| if (errorMsgs.size() > 0) { |
| result = ServiceUtil.returnError(errorMsgs); |
| result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_SUCCESS); |
| return result; // don't return error because this is a non-critical error and should go back to the same page |
| } |
| |
| result = ServiceUtil.returnSuccess(); |
| result.put("totalQuantity", totalQuantity); |
| return result; |
| } |
| |
| /** Delete an item from the shopping cart. */ |
| public Map<String, Object> deleteFromCart(Map<String, ? extends Object> context) { |
| Map<String, Object> result = null; |
| Set<String> names = context.keySet(); |
| Iterator<String> i = names.iterator(); |
| ArrayList<String> errorMsgs = new ArrayList<String>(); |
| |
| while (i.hasNext()) { |
| String o = i.next(); |
| |
| if (o.toUpperCase().startsWith("DELETE")) { |
| try { |
| String indexStr = o.substring(o.lastIndexOf('_') + 1); |
| int index = Integer.parseInt(indexStr); |
| |
| try { |
| this.cart.removeCartItem(index, dispatcher); |
| } catch (CartItemModifyException e) { |
| errorMsgs.add(e.getMessage()); |
| } |
| } catch (NumberFormatException nfe) {} |
| } |
| } |
| |
| if (errorMsgs.size() > 0) { |
| result = ServiceUtil.returnError(errorMsgs); |
| result.put(ModelService.RESPONSE_MESSAGE, ModelService.RESPOND_SUCCESS); |
| return result; // don't return error because this is a non-critical error and should go back to the same page |
| } |
| |
| result = ServiceUtil.returnSuccess(); |
| return result; |
| } |
| |
| /** Update the items in the shopping cart. */ |
| public Map<String, Object> modifyCart(Security security, GenericValue userLogin, Map<String, ? extends Object> context, boolean removeSelected, String[] selectedItems, Locale locale) { |
| Map<String, Object> result = null; |
| if (locale == null) { |
| locale = this.cart.getLocale(); |
| } |
| |
| ArrayList<ShoppingCartItem> deleteList = new ArrayList<ShoppingCartItem>(); |
| ArrayList<String> errorMsgs = new ArrayList<String>(); |
| |
| Set<String> parameterNames = context.keySet(); |
| Iterator<String> parameterNameIter = parameterNames.iterator(); |
| |
| BigDecimal oldQuantity = BigDecimal.ONE.negate(); |
| String oldDescription = ""; |
| BigDecimal oldPrice = BigDecimal.ONE.negate(); |
| |
| if (this.cart.isReadOnlyCart()) { |
| String errMsg = UtilProperties.getMessage(resource_error, "cart.cart_is_in_read_only_mode", this.cart.getLocale()); |
| errorMsgs.add(errMsg); |
| result = ServiceUtil.returnError(errorMsgs); |
| return result; |
| } |
| |
| // TODO: This should be refactored to use UtilHttp.parseMultiFormData(parameters) |
| while (parameterNameIter.hasNext()) { |
| String parameterName = parameterNameIter.next(); |
| int underscorePos = parameterName.lastIndexOf('_'); |
| |
| if (underscorePos >= 0) { |
| try { |
| String indexStr = parameterName.substring(underscorePos + 1); |
| int index = Integer.parseInt(indexStr); |
| String quantString = (String) context.get(parameterName); |
| BigDecimal quantity = BigDecimal.ONE.negate(); |
| String itemDescription = ""; |
| if (quantString != null) quantString = quantString.trim(); |
| |
| // get the cart item |
| ShoppingCartItem item = this.cart.findCartItem(index); |
| if (parameterName.toUpperCase().startsWith("OPTION")) { |
| if (quantString.toUpperCase().startsWith("NO^")) { |
| if (quantString.length() > 2) { // the length of the prefix |
| String featureTypeId = this.getRemoveFeatureTypeId(parameterName); |
| if (featureTypeId != null) { |
| item.removeAdditionalProductFeatureAndAppl(featureTypeId); |
| } |
| } |
| } else { |
| GenericValue featureAppl = this.getFeatureAppl(item.getProductId(), parameterName, quantString); |
| if (featureAppl != null) { |
| item.putAdditionalProductFeatureAndAppl(featureAppl); |
| } |
| } |
| } else if (parameterName.toUpperCase().startsWith("DESCRIPTION")) { |
| itemDescription = quantString; // the quantString is actually the description if the field name starts with DESCRIPTION |
| } else if (parameterName.startsWith("reservStart")) { |
| if (quantString.length() ==0) { |
| // should have format: yyyy-mm-dd hh:mm:ss.fffffffff |
| quantString += " 00:00:00.000000000"; |
| } |
| if (item != null) { |
| Timestamp reservStart = Timestamp.valueOf(quantString); |
| item.setReservStart(reservStart); |
| } |
| } else if (parameterName.startsWith("reservLength")) { |
| if (item != null) { |
| BigDecimal reservLength = (BigDecimal) ObjectType.simpleTypeConvert(quantString, "BigDecimal", null, locale); |
| item.setReservLength(reservLength); |
| } |
| } else if (parameterName.startsWith("reservPersons")) { |
| if (item != null) { |
| BigDecimal reservPersons = (BigDecimal) ObjectType.simpleTypeConvert(quantString, "BigDecimal", null, locale); |
| item.setReservPersons(reservPersons); |
| } |
| } else if (parameterName.startsWith("shipBeforeDate")) { |
| if (UtilValidate.isNotEmpty(quantString)) { |
| // input is either yyyy-mm-dd or a full timestamp |
| if (quantString.length() == 10) |
| quantString += " 00:00:00.000"; |
| item.setShipBeforeDate(Timestamp.valueOf(quantString)); |
| } |
| } else if (parameterName.startsWith("shipAfterDate")) { |
| if (UtilValidate.isNotEmpty(quantString)) { |
| // input is either yyyy-mm-dd or a full timestamp |
| if (quantString.length() == 10) |
| quantString += " 00:00:00.000"; |
| item.setShipAfterDate(Timestamp.valueOf(quantString)); |
| } |
| } else if (parameterName.startsWith("amount")) { |
| if (UtilValidate.isNotEmpty(quantString)) { |
| BigDecimal amount = new BigDecimal(quantString); |
| if (amount.compareTo(BigDecimal.ZERO) <= 0) { |
| String errMsg = UtilProperties.getMessage(resource_error, "cart.amount_not_positive_number", this.cart.getLocale()); |
| errorMsgs.add(errMsg); |
| result = ServiceUtil.returnError(errorMsgs); |
| return result; |
| } |
| item.setSelectedAmount(amount); |
| } |
| } else if (parameterName.startsWith("itemType")) { |
| if (UtilValidate.isNotEmpty(quantString)) { |
| item.setItemType(quantString); |
| } |
| } else { |
| quantity = (BigDecimal) ObjectType.simpleTypeConvert(quantString, "BigDecimal", null, locale); |
| //For quantity we should test if we allow to add decimal quantity for this product an productStore : if not then round to 0 |
| if(! ProductWorker.isDecimalQuantityOrderAllowed(delegator, item.getProductId(), cart.getProductStoreId())){ |
| quantity = quantity.setScale(0, UtilNumber.getBigDecimalRoundingMode("order.rounding")); |
| } |
| else { |
| quantity = quantity.setScale(UtilNumber.getBigDecimalScale("order.decimals"), UtilNumber.getBigDecimalRoundingMode("order.rounding")); |
| } |
| if (quantity.compareTo(BigDecimal.ZERO) < 0) { |
| String errMsg = UtilProperties.getMessage(resource_error, "cart.quantity_not_positive_number", this.cart.getLocale()); |
| errorMsgs.add(errMsg); |
| result = ServiceUtil.returnError(errorMsgs); |
| return result; |
| } |
| } |
| |
| // perhaps we need to reset the ship groups' before and after dates based on new dates for the items |
| if (parameterName.startsWith("shipAfterDate") || parameterName.startsWith("shipBeforeDate")) { |
| this.cart.setShipGroupShipDatesFromItem(item); |
| } |
| |
| if (parameterName.toUpperCase().startsWith("UPDATE")) { |
| if (quantity.compareTo(BigDecimal.ZERO) == 0) { |
| deleteList.add(item); |
| } else { |
| if (item != null) { |
| try { |
| // if, on a purchase order, the quantity has changed, get the new SupplierProduct entity for this quantity level. |
| if (cart.getOrderType().equals("PURCHASE_ORDER")) { |
| oldQuantity = item.getQuantity(); |
| if (oldQuantity.compareTo(quantity) != 0) { |
| // save the old description and price, in case the user wants to change those as well |
| oldDescription = item.getName(); |
| oldPrice = item.getBasePrice(); |
| |
| |
| GenericValue supplierProduct = this.cart.getSupplierProduct(item.getProductId(), quantity, this.dispatcher); |
| |
| if (supplierProduct == null) { |
| if ("_NA_".equals(cart.getPartyId())) { |
| // no supplier does not require the supplier product |
| item.setQuantity(quantity, dispatcher, this.cart); |
| item.setName(item.getProduct().getString("internalName")); |
| } else { |
| // in this case, the user wanted to purchase a quantity which is not available (probably below minimum) |
| String errMsg = UtilProperties.getMessage(resource_error, "cart.product_not_valid_for_supplier", this.cart.getLocale()); |
| errMsg = errMsg + " (" + item.getProductId() + ", " + quantity + ", " + cart.getCurrency() + ")"; |
| errorMsgs.add(errMsg); |
| } |
| } else { |
| item.setSupplierProductId(supplierProduct.getString("supplierProductId")); |
| item.setQuantity(quantity, dispatcher, this.cart); |
| item.setBasePrice(supplierProduct.getBigDecimal("lastPrice")); |
| item.setName(ShoppingCartItem.getPurchaseOrderItemDescription(item.getProduct(), supplierProduct, cart.getLocale())); |
| } |
| } |
| } else { |
| BigDecimal minQuantity = ShoppingCart.getMinimumOrderQuantity(delegator, item.getBasePrice(), item.getProductId()); |
| if (quantity.compareTo(minQuantity) < 0) { |
| quantity = minQuantity; |
| } |
| item.setQuantity(quantity, dispatcher, this.cart, true, false); |
| cart.setItemShipGroupQty(item, quantity, 0); |
| } |
| } catch (CartItemModifyException e) { |
| errorMsgs.add(e.getMessage()); |
| } |
| } |
| } |
| } |
| |
| if (parameterName.toUpperCase().startsWith("DESCRIPTION")) { |
| if (!oldDescription.equals(itemDescription)) { |
| if (security.hasEntityPermission("ORDERMGR", "_CREATE", userLogin)) { |
| if (item != null) { |
| item.setName(itemDescription); |
| } |
| } |
| } |
| } |
| |
| if (parameterName.toUpperCase().startsWith("PRICE")) { |
| NumberFormat pf = NumberFormat.getCurrencyInstance(locale); |
| String tmpQuantity = pf.format(quantity); |
| String tmpOldPrice = pf.format(oldPrice); |
| if (!tmpOldPrice.equals(tmpQuantity)) { |
| if (security.hasEntityPermission("ORDERMGR", "_CREATE", userLogin)) { |
| if (item != null) { |
| item.setBasePrice(quantity); // this is quantity because the parsed number variable is the same as quantity |
| item.setDisplayPrice(quantity); // or the amount shown the cart items page won't be right |
| item.setIsModifiedPrice(true); // flag as a modified price |
| } |
| } |
| } |
| } |
| |
| if (parameterName.toUpperCase().startsWith("DELETE")) { |
| deleteList.add(this.cart.findCartItem(index)); |
| } |
| } catch (NumberFormatException nfe) { |
| Debug.logWarning(nfe, UtilProperties.getMessage(resource_error, "OrderCaughtNumberFormatExceptionOnCartUpdate", cart.getLocale())); |
| } catch (Exception e) { |
| Debug.logWarning(e, UtilProperties.getMessage(resource_error, "OrderCaughtExceptionOnCartUpdate", cart.getLocale())); |
| } |
| } // else not a parameter we need |
| } |
| |
| // get a list of the items to delete |
| if (removeSelected) { |
| for (int si = 0; si < selectedItems.length; si++) { |
| String indexStr = selectedItems[si]; |
| ShoppingCartItem item = null; |
| try { |
| int index = Integer.parseInt(indexStr); |
| item = this.cart.findCartItem(index); |
| } catch (Exception e) { |
| Debug.logWarning(e, UtilProperties.getMessage(resource_error, "OrderProblemsGettingTheCartItemByIndex", cart.getLocale())); |
| } |
| if (item != null) { |
| deleteList.add(item); |
| } |
| } |
| } |
| |
| Iterator<ShoppingCartItem> di = deleteList.iterator(); |
| |
| while (di.hasNext()) { |
| ShoppingCartItem item = di.next(); |
| int itemIndex = this.cart.getItemIndex(item); |
| |
| if (Debug.infoOn()) |
| Debug.logInfo("Removing item index: " + itemIndex, module); |
| try { |
| this.cart.removeCartItem(itemIndex, dispatcher); |
| } catch (CartItemModifyException e) { |
| result = ServiceUtil.returnError(new ArrayList<String>()); |
| errorMsgs.add(e.getMessage()); |
| } |
| } |
| |
| if (context.containsKey("alwaysShowcart")) { |
| this.cart.setViewCartOnAdd(true); |
| } else { |
| this.cart.setViewCartOnAdd(false); |
| } |
| |
| // Promotions are run again. |
| ProductPromoWorker.doPromotions(this.cart, dispatcher); |
| |
| if (errorMsgs.size() > 0) { |
| result = ServiceUtil.returnError(errorMsgs); |
| return result; |
| } |
| |
| result = ServiceUtil.returnSuccess(); |
| return result; |
| } |
| |
| /** Empty the shopping cart. */ |
| public boolean clearCart() { |
| this.cart.clear(); |
| return true; |
| } |
| |
| /** Returns the shopping cart this helper is wrapping. */ |
| public ShoppingCart getCartObject() { |
| return this.cart; |
| } |
| |
| public GenericValue getFeatureAppl(String productId, String optionField, String featureId) { |
| if (delegator == null) { |
| throw new IllegalArgumentException("No delegator available to lookup ProductFeature"); |
| } |
| |
| Map<String, String> fields = UtilMisc.<String, String>toMap("productId", productId, "productFeatureId", featureId); |
| if (optionField != null) { |
| int featureTypeStartIndex = optionField.indexOf('^') + 1; |
| int featureTypeEndIndex = optionField.lastIndexOf('_'); |
| if (featureTypeStartIndex > 0 && featureTypeEndIndex > 0) { |
| fields.put("productFeatureTypeId", optionField.substring(featureTypeStartIndex, featureTypeEndIndex)); |
| } |
| } |
| |
| GenericValue productFeatureAppl = null; |
| List<GenericValue> features = null; |
| try { |
| features = delegator.findByAnd("ProductFeatureAndAppl", fields, UtilMisc.toList("-fromDate")); |
| } catch (GenericEntityException e) { |
| Debug.logError(e, module); |
| return null; |
| } |
| |
| if (features != null) { |
| if (features.size() > 1) { |
| features = EntityUtil.filterByDate(features); |
| } |
| productFeatureAppl = EntityUtil.getFirst(features); |
| } |
| |
| return productFeatureAppl; |
| } |
| |
| public String getRemoveFeatureTypeId(String optionField) { |
| if (optionField != null) { |
| int featureTypeStartIndex = optionField.indexOf('^') + 1; |
| int featureTypeEndIndex = optionField.lastIndexOf('_'); |
| if (featureTypeStartIndex > 0 && featureTypeEndIndex > 0) { |
| return optionField.substring(featureTypeStartIndex, featureTypeEndIndex); |
| } |
| } |
| return null; |
| } |
| /** |
| * Select an agreement |
| * |
| * @param agreementId |
| */ |
| public Map<String, Object> selectAgreement(String agreementId) { |
| Map<String, Object> result = null; |
| GenericValue agreement = null; |
| |
| if ((this.delegator == null) || (this.dispatcher == null) || (this.cart == null)) { |
| result = ServiceUtil.returnError(UtilProperties.getMessage(resource_error,"OrderDispatcherOrDelegatorOrCartArgumentIsNull",this.cart.getLocale())); |
| return result; |
| } |
| |
| if ((agreementId == null) || (agreementId.length() <= 0)) { |
| result = ServiceUtil.returnError(UtilProperties.getMessage(resource_error,"OrderNoAgreementSpecified",this.cart.getLocale())); |
| return result; |
| } |
| |
| try { |
| agreement = this.delegator.findByPrimaryKeyCache("Agreement",UtilMisc.toMap("agreementId", agreementId)); |
| } catch (GenericEntityException e) { |
| Debug.logWarning(e.toString(), module); |
| result = ServiceUtil.returnError(UtilProperties.getMessage(resource_error,"OrderCouldNotGetAgreement",UtilMisc.toMap("agreementId",agreementId),this.cart.getLocale()) + UtilProperties.getMessage(resource_error,"OrderError",this.cart.getLocale()) + e.getMessage()); |
| return result; |
| } |
| |
| if (agreement == null) { |
| result = ServiceUtil.returnError(UtilProperties.getMessage(resource_error,"OrderCouldNotGetAgreement",UtilMisc.toMap("agreementId",agreementId),this.cart.getLocale())); |
| } else { |
| // set the agreement id in the cart |
| cart.setAgreementId(agreementId); |
| try { |
| // set the currency based on the pricing agreement |
| List<GenericValue> agreementItems = agreement.getRelated("AgreementItem", UtilMisc.toMap("agreementItemTypeId", "AGREEMENT_PRICING_PR"), null); |
| if (agreementItems.size() > 0) { |
| GenericValue agreementItem = agreementItems.get(0); |
| String currencyUomId = (String) agreementItem.get("currencyUomId"); |
| if (UtilValidate.isNotEmpty(currencyUomId)) { |
| try { |
| cart.setCurrency(dispatcher,currencyUomId); |
| } catch (CartItemModifyException ex) { |
| result = ServiceUtil.returnError(UtilProperties.getMessage(resource_error,"OrderSetCurrencyError",this.cart.getLocale()) + ex.getMessage()); |
| return result; |
| } |
| } |
| } |
| } catch (GenericEntityException e) { |
| Debug.logWarning(e.toString(), module); |
| result = ServiceUtil.returnError(UtilProperties.getMessage(resource_error,"OrderCouldNotGetAgreementItemsThrough",UtilMisc.toMap("agreementId",agreementId),this.cart.getLocale()) + UtilProperties.getMessage(resource_error,"OrderError",this.cart.getLocale()) + e.getMessage()); |
| return result; |
| } |
| |
| try { |
| // clear the existing order terms |
| cart.removeOrderTerms(); |
| // set order terms based on agreement terms |
| List<GenericValue> agreementTerms = EntityUtil.filterByDate(agreement.getRelated("AgreementTerm")); |
| if (agreementTerms.size() > 0) { |
| for (int i = 0; agreementTerms.size() > i;i++) { |
| GenericValue agreementTerm = agreementTerms.get(i); |
| String termTypeId = (String) agreementTerm.get("termTypeId"); |
| BigDecimal termValue = agreementTerm.getBigDecimal("termValue"); |
| Long termDays = (Long) agreementTerm.get("termDays"); |
| String textValue = agreementTerm.getString("textValue"); |
| cart.addOrderTerm(termTypeId, termValue, termDays, textValue); |
| } |
| } |
| } catch (GenericEntityException e) { |
| Debug.logWarning(e.toString(), module); |
| result = ServiceUtil.returnError(UtilProperties.getMessage(resource_error,"OrderCouldNotGetAgreementTermsThrough",UtilMisc.toMap("agreementId",agreementId),this.cart.getLocale()) + UtilProperties.getMessage(resource_error,"OrderError",this.cart.getLocale()) + e.getMessage()); |
| return result; |
| } |
| } |
| return result; |
| } |
| |
| public Map<String, Object> setCurrency(String currencyUomId) { |
| Map<String, Object> result = null; |
| |
| try { |
| this.cart.setCurrency(this.dispatcher,currencyUomId); |
| result = ServiceUtil.returnSuccess(); |
| } catch (CartItemModifyException ex) { |
| result = ServiceUtil.returnError(UtilProperties.getMessage(resource_error,"OrderSetCurrencyError",this.cart.getLocale()) + ex.getMessage()); |
| return result; |
| } |
| return result; |
| } |
| |
| public Map<String, Object> addOrderTerm(String termTypeId, BigDecimal termValue, Long termDays) { |
| return addOrderTerm(termTypeId, termValue, termDays, null); |
| } |
| |
| public Map<String, Object> addOrderTerm(String termTypeId, BigDecimal termValue,Long termDays, String textValue) { |
| Map<String, Object> result = null; |
| this.cart.addOrderTerm(termTypeId,termValue,termDays,textValue); |
| result = ServiceUtil.returnSuccess(); |
| return result; |
| } |
| |
| public Map<String, Object> removeOrderTerm(int index) { |
| Map<String, Object> result = null; |
| this.cart.removeOrderTerm(index); |
| result = ServiceUtil.returnSuccess(); |
| return result; |
| } |
| |
| } |