Author: lektran
Date: Tue Jul 28 12:13:28 2009 New Revision: 798500 URL: http://svn.apache.org/viewvc?rev=798500&view=rev Log: Implemented a services to keep track of products purchased together by creating ProductAssoc records of type ALSO_BOUGHT. These associated products are then shown to the customer on the product pages as "Customers who bought this item also bought: ..." sorted by popularity. Modified: ofbiz/trunk/applications/order/data/OrderScheduledServices.xml ofbiz/trunk/applications/order/servicedef/services.xml ofbiz/trunk/applications/order/src/org/ofbiz/order/order/OrderReadHelper.java ofbiz/trunk/applications/order/src/org/ofbiz/order/order/OrderServices.java ofbiz/trunk/applications/order/webapp/ordermgr/WEB-INF/actions/entry/catalog/ProductDetail.groovy ofbiz/trunk/applications/product/config/ProductUiLabels.xml ofbiz/trunk/applications/product/servicedef/services_view.xml ofbiz/trunk/applications/product/src/org/ofbiz/product/product/ProductServices.java ofbiz/trunk/specialpurpose/ecommerce/webapp/ecommerce/catalog/productdetail.ftl Modified: ofbiz/trunk/applications/order/data/OrderScheduledServices.xml URL: http://svn.apache.org/viewvc/ofbiz/trunk/applications/order/data/OrderScheduledServices.xml?rev=798500&r1=798499&r2=798500&view=diff ============================================================================== --- ofbiz/trunk/applications/order/data/OrderScheduledServices.xml (original) +++ ofbiz/trunk/applications/order/data/OrderScheduledServices.xml Tue Jul 28 12:13:28 2009 @@ -29,5 +29,6 @@ <JobSandbox jobId="8005" jobName="Extend expired Subscriptions" runTime="2000-01-01 03:00:00.000" serviceName="runSubscriptionAutoReorders" poolId="pool" runAsUser="system" tempExprId="MIDNIGHT_DAILY" maxRecurrenceCount="-1"/> <JobSandbox jobId="8006" jobName="Cancels all orders after date" runTime="2009-12-03 03:00:00.000" serviceName="cancelAllBackOrders" poolId="pool" runAsUser="system" tempExprId="MIDNIGHT_DAILY" maxRecurrenceCount="-1"/> <JobSandbox jobId="8007" jobName="Replacement Held Order Auto-Cancel" runTime="2000-01-01 00:00:00.000" serviceName="autoCancelReplacementOrders" poolId="pool" runAsUser="system" tempExprId="MIDNIGHT_DAILY" maxRecurrenceCount="-1"/> + <JobSandbox jobId="8008" jobName="Create Also Bought Product Associations" runTime="2000-01-01 00:00:00.000" serviceName="createAlsoBoughtProductAssocs" poolId="pool" runAsUser="system" tempExprId="MIDNIGHT_DAILY" maxRecurrenceCount="-1"/> </entity-engine-xml> Modified: ofbiz/trunk/applications/order/servicedef/services.xml URL: http://svn.apache.org/viewvc/ofbiz/trunk/applications/order/servicedef/services.xml?rev=798500&r1=798499&r2=798500&view=diff ============================================================================== --- ofbiz/trunk/applications/order/servicedef/services.xml (original) +++ ofbiz/trunk/applications/order/servicedef/services.xml Tue Jul 28 12:13:28 2009 @@ -1008,4 +1008,25 @@ <attribute name="shipGroupSeqId" type="String" mode="IN" optional="false"/> <attribute name="giftMessage" type="String" mode="IN" optional="true"/> </service> + <service name="createAlsoBoughtProductAssocs" engine="java" auth="true" + location="org.ofbiz.order.order.OrderServices" invoke="createAlsoBoughtProductAssocs"> + <description> + Cycles through all newly created sales orders and creates ProductAssoc records (of type ALSO_BOUGHT) for products + that were purchased together. If a ProductAssoc record already exists then the quantity field is incremented by one. + Newly created orders are determined by looking for orders that were created after the JobSandbox.startDateTime of the + previous async execution of this service, alternatively the service can be supplied with a orderEntryFromDateTime + parameter which will process all orders placed after that date/time or as a final option processAllOrders can be set + to true to force a calculation of all orders ever placed with orderEntryFromDateTime being ignored. + </description> + <attribute name="orderEntryFromDateTime" mode="IN" type="Timestamp" optional="true"/> + <attribute name="processAllOrders" mode="IN" type="Boolean" optional="true"/> + </service> + <service name="createAlsoBoughtProductAssocsForOrder" engine="java" auth="true" + location="org.ofbiz.order.order.OrderServices" invoke="createAlsoBoughtProductAssocsForOrder"> + <description> + Creates ProductAssoc records (of type ALSO_BOUGHT) for products that were purchased together in the Order. If a ProductAssoc record already exists then the quantity field is incremented by one. If a variant product has + been ordered then the association is made to its parent product. + </description> + <attribute name="orderId" mode="IN" type="String" optional="false"/> + </service> </services> \ No newline at end of file Modified: ofbiz/trunk/applications/order/src/org/ofbiz/order/order/OrderReadHelper.java URL: http://svn.apache.org/viewvc/ofbiz/trunk/applications/order/src/org/ofbiz/order/order/OrderReadHelper.java?rev=798500&r1=798499&r2=798500&view=diff ============================================================================== --- ofbiz/trunk/applications/order/src/org/ofbiz/order/order/OrderReadHelper.java (original) +++ ofbiz/trunk/applications/order/src/org/ofbiz/order/order/OrderReadHelper.java Tue Jul 28 12:13:28 2009 @@ -75,7 +75,7 @@ protected GenericValue orderHeader = null; protected List orderItemAndShipGrp = null; - protected List orderItems = null; + protected List<GenericValue> orderItems = null; protected List adjustments = null; protected List<GenericValue> paymentPrefs = null; protected List orderStatuses = null; @@ -1366,7 +1366,7 @@ // ========== Order Item Methods ========== // ======================================== - public List getOrderItems() { + public List<GenericValue> getOrderItems() { if (orderItems == null) { try { orderItems = orderHeader.getRelated("OrderItem", UtilMisc.toList("orderItemSeqId")); Modified: ofbiz/trunk/applications/order/src/org/ofbiz/order/order/OrderServices.java URL: http://svn.apache.org/viewvc/ofbiz/trunk/applications/order/src/org/ofbiz/order/order/OrderServices.java?rev=798500&r1=798499&r2=798500&view=diff ============================================================================== --- ofbiz/trunk/applications/order/src/org/ofbiz/order/order/OrderServices.java (original) +++ ofbiz/trunk/applications/order/src/org/ofbiz/order/order/OrderServices.java Tue Jul 28 12:13:28 2009 @@ -21,7 +21,6 @@ import java.math.BigDecimal; import java.sql.Timestamp; import java.util.ArrayList; -import com.ibm.icu.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashMap; @@ -31,6 +30,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import javax.transaction.Transaction; @@ -56,10 +56,10 @@ import org.ofbiz.entity.condition.EntityCondition; import org.ofbiz.entity.condition.EntityConditionList; import org.ofbiz.entity.condition.EntityExpr; -import org.ofbiz.entity.condition.EntityJoinOperator; import org.ofbiz.entity.condition.EntityOperator; import org.ofbiz.entity.transaction.GenericTransactionException; import org.ofbiz.entity.transaction.TransactionUtil; +import org.ofbiz.entity.util.EntityFindOptions; import org.ofbiz.entity.util.EntityListIterator; import org.ofbiz.entity.util.EntityUtil; import org.ofbiz.order.shoppingcart.CartItemModifyException; @@ -81,6 +81,8 @@ import org.ofbiz.service.ModelService; import org.ofbiz.service.ServiceUtil; +import com.ibm.icu.util.Calendar; + /** * Order Processing Services */ @@ -91,8 +93,8 @@ public static final String resource = "OrderUiLabels"; public static final String resource_error = "OrderErrorUiLabels"; - public static Map salesAttributeRoleMap = FastMap.newInstance(); - public static Map purchaseAttributeRoleMap = FastMap.newInstance(); + public static Map<String, String> salesAttributeRoleMap = FastMap.newInstance(); + public static Map<String, String> purchaseAttributeRoleMap = FastMap.newInstance(); static { salesAttributeRoleMap.put("placingCustomerPartyId", "PLACING_CUSTOMER"); salesAttributeRoleMap.put("billToCustomerPartyId", "BILL_TO_CUSTOMER"); @@ -145,7 +147,7 @@ hasPermission = true; } else { // check sales agent/customer relationship - List repsCustomers = new LinkedList(); + List<GenericValue> repsCustomers = new LinkedList<GenericValue>(); try { repsCustomers = EntityUtil.filterByDate(userLogin.getRelatedOne("Party").getRelatedByAnd("FromPartyRelationship", UtilMisc.toMap("roleTypeIdFrom", "AGENT", "roleTypeIdTo", "CUSTOMER", "partyIdTo", partyId))); @@ -5300,4 +5302,145 @@ } return ServiceUtil.returnSuccess(); } + + public static Map<String, Object> createAlsoBoughtProductAssocs(DispatchContext dctx, Map context) { + GenericDelegator delegator = dctx.getDelegator(); + LocalDispatcher dispatcher = dctx.getDispatcher(); + // All orders with an entryDate > orderEntryFromDateTime will be processed + Timestamp orderEntryFromDateTime = (Timestamp) context.get("orderEntryFromDateTime"); + // If true all orders ever created will be processed and any pre-existing ALSO_BOUGHT ProductAssocs will be expired + boolean processAllOrders = context.get("processAllOrders") == null ? false : (Boolean) context.get("processAllOrders"); + if (orderEntryFromDateTime == null && !processAllOrders) { + // No from date supplied, check to see when this service last ran and use the startDateTime + EntityCondition cond = EntityCondition.makeCondition(UtilMisc.toMap("statusId", "SERVICE_FINISHED", "serviceName", "createAlsoBoughtProductAssocs")); + EntityFindOptions efo = new EntityFindOptions(); + efo.setMaxRows(1); + try { + GenericValue lastRunJobSandbox = EntityUtil.getFirst(delegator.findList("JobSandbox", cond, null, UtilMisc.toList("startDateTime DESC"), efo, false)); + if (lastRunJobSandbox != null) { + orderEntryFromDateTime = lastRunJobSandbox.getTimestamp("startDateTime"); + } + } catch (GenericEntityException e) { + Debug.logError(e, module); + } + if (orderEntryFromDateTime == null) { + // Still null, process all orders + processAllOrders = true; + } + } + if (processAllOrders) { + // Expire any pre-existing ALSO_BOUGHT ProductAssocs in preparation for reprocessing + EntityCondition cond = EntityCondition.makeCondition(UtilMisc.toList( + EntityCondition.makeCondition("productAssocTypeId", "ALSO_BOUGHT"), + EntityCondition.makeConditionDate("fromDate", "thruDate") + )); + try { + delegator.storeByCondition("ProductAssoc", UtilMisc.toMap("thruDate", UtilDateTime.nowTimestamp()), cond); + } catch (GenericEntityException e) { + Debug.logError(e, module); + } + } + EntityListIterator eli = null; + try { + List<EntityExpr> orderCondList = UtilMisc.toList(EntityCondition.makeCondition("orderTypeId", "SALES_ORDER")); + if (!processAllOrders && orderEntryFromDateTime != null) { + orderCondList.add(EntityCondition.makeCondition("entryDate", EntityOperator.GREATER_THAN, orderEntryFromDateTime)); + } + EntityCondition cond = EntityCondition.makeCondition(orderCondList); + eli = delegator.find("OrderHeader", cond, null, null, UtilMisc.toList("entryDate ASC"), null); + } catch (GenericEntityException e) { + Debug.logError(e, module); + return ServiceUtil.returnError(e.getMessage()); + } + if (eli != null) { + GenericValue orderHeader = null; + while ((orderHeader = eli.next()) != null) { + Map svcIn = FastMap.newInstance(); + svcIn.put("userLogin", context.get("userLogin")); + svcIn.put("orderId", orderHeader.get("orderId")); + try { + dispatcher.runSync("createAlsoBoughtProductAssocsForOrder", svcIn); + } catch (GenericServiceException e) { + Debug.logError(e, module); + } + } + try { + eli.close(); + } catch (GenericEntityException e) { + Debug.logError(e, module); + } + } + return ServiceUtil.returnSuccess(); + } + + public static Map<String, Object> createAlsoBoughtProductAssocsForOrder(DispatchContext dctx, Map context) { + LocalDispatcher dispatcher = dctx.getDispatcher(); + GenericDelegator delegator = dctx.getDelegator(); + String orderId = (String) context.get("orderId"); + OrderReadHelper orh = new OrderReadHelper(delegator, orderId); + List<GenericValue> orderItems = orh.getOrderItems(); + // In order to improve efficiency a little bit, we will always create the ProductAssoc records + // with productId < productIdTo when the two are compared. This way when checking for an existing + // record we don't have to check both possible combinations of productIds + TreeSet<String> productIdSet = new TreeSet<String>(); + if (orderItems != null) { + for (GenericValue orderItem : orderItems) { + String productId = orderItem.getString("productId"); + if (productId != null) { + GenericValue parentProduct = ProductWorker.getParentProduct(productId, delegator); + if (parentProduct != null) productId = parentProduct.getString("productId"); + productIdSet.add(productId); + } + } + } + TreeSet<String> productIdToSet = new TreeSet<String>(productIdSet); + for (String productId : productIdSet) { + productIdToSet.remove(productId); + for (String productIdTo : productIdToSet) { + EntityCondition cond = EntityCondition.makeCondition( + UtilMisc.toList( + EntityCondition.makeCondition("productId", productId), + EntityCondition.makeCondition("productIdTo", productIdTo), + EntityCondition.makeCondition("productAssocTypeId", "ALSO_BOUGHT"), + EntityCondition.makeCondition("fromDate", EntityOperator.LESS_THAN_EQUAL_TO, UtilDateTime.nowTimestamp()), + EntityCondition.makeCondition("thruDate", null) + ) + ); + GenericValue existingProductAssoc = null; + try { + // No point in using the cache because of the filterByDateExpr + existingProductAssoc = EntityUtil.getFirst(delegator.findList("ProductAssoc", cond, null, UtilMisc.toList("fromDate DESC"), null, false)); + } catch (GenericEntityException e) { + Debug.logError(e, module); + } + try { + if (existingProductAssoc != null) { + BigDecimal newQuantity = existingProductAssoc.getBigDecimal("quantity"); + if (newQuantity == null || newQuantity.compareTo(BigDecimal.ZERO) < 0) { + newQuantity = BigDecimal.ZERO; + } + newQuantity = newQuantity.add(BigDecimal.ONE); + ModelService updateProductAssoc = dctx.getModelService("updateProductAssoc"); + Map<String, Object> updateCtx = updateProductAssoc.makeValid(context, ModelService.IN_PARAM, true, null); + updateCtx.putAll(updateProductAssoc.makeValid(existingProductAssoc, ModelService.IN_PARAM)); + updateCtx.put("quantity", newQuantity); + dispatcher.runSync("updateProductAssoc", updateCtx); + } else { + Map<String, Object> createCtx = FastMap.newInstance(); + createCtx.put("userLogin", context.get("userLogin")); + createCtx.put("productId", productId); + createCtx.put("productIdTo", productIdTo); + createCtx.put("productAssocTypeId", "ALSO_BOUGHT"); + createCtx.put("fromDate", UtilDateTime.nowTimestamp()); + createCtx.put("quantity", BigDecimal.ONE); + dispatcher.runSync("createProductAssoc", createCtx); + } + } catch (GenericServiceException e) { + Debug.logError(e, module); + } + } + } + + return ServiceUtil.returnSuccess(); + } } Modified: ofbiz/trunk/applications/order/webapp/ordermgr/WEB-INF/actions/entry/catalog/ProductDetail.groovy URL: http://svn.apache.org/viewvc/ofbiz/trunk/applications/order/webapp/ordermgr/WEB-INF/actions/entry/catalog/ProductDetail.groovy?rev=798500&r1=798499&r2=798500&view=diff ============================================================================== --- ofbiz/trunk/applications/order/webapp/ordermgr/WEB-INF/actions/entry/catalog/ProductDetail.groovy (original) +++ ofbiz/trunk/applications/order/webapp/ordermgr/WEB-INF/actions/entry/catalog/ProductDetail.groovy Tue Jul 28 12:13:28 2009 @@ -379,6 +379,9 @@ } // get product associations + alsoBoughtProducts = dispatcher.runSync("getAssociatedProducts", [productId : productId, type : "ALSO_BOUGHT", checkViewAllow : true, prodCatalogId : currentCatalogId, bidirectional : true, sortDescending : true]); + context.alsoBoughtProducts = alsoBoughtProducts.assocProducts; + obsoleteProducts = dispatcher.runSync("getAssociatedProducts", [productId : productId, type : "PRODUCT_OBSOLESCENCE", checkViewAllow : true, prodCatalogId : currentCatalogId]); context.obsoleteProducts = obsoleteProducts.assocProducts; Modified: ofbiz/trunk/applications/product/config/ProductUiLabels.xml URL: http://svn.apache.org/viewvc/ofbiz/trunk/applications/product/config/ProductUiLabels.xml?rev=798500&r1=798499&r2=798500&view=diff ============================================================================== --- ofbiz/trunk/applications/product/config/ProductUiLabels.xml (original) +++ ofbiz/trunk/applications/product/config/ProductUiLabels.xml Tue Jul 28 12:13:28 2009 @@ -7061,6 +7061,9 @@ <value xml:lang="th">Allow USPS Addr (PO Box, RR, etc)</value> <value xml:lang="zh">å 许USPSå°å (é®ç®±ã éè´§çç)</value> </property> + <property key="ProductAlsoBought"> + <value xml:lang="en">Customers who bought this item also bought:</value> + </property> <property key="ProductAlternate"> <value xml:lang="de">Alternative</value> <value xml:lang="en">Alternate</value> Modified: ofbiz/trunk/applications/product/servicedef/services_view.xml URL: http://svn.apache.org/viewvc/ofbiz/trunk/applications/product/servicedef/services_view.xml?rev=798500&r1=798499&r2=798500&view=diff ============================================================================== --- ofbiz/trunk/applications/product/servicedef/services_view.xml (original) +++ ofbiz/trunk/applications/product/servicedef/services_view.xml Tue Jul 28 12:13:28 2009 @@ -63,12 +63,19 @@ </service> <service name="getAssociatedProducts" engine="java" location="org.ofbiz.product.product.ProductServices" invoke="prodFindAssociatedByType"> - <description>Finds associated products by the defined type.</description> + <description> + Finds associated products by the defined type. Only one of either productId or productIdTo can be supplied, + not both. If bidirectional is set to true then the passed in productId will be treated as both a productId + and a productIdTo (defaults to false). If sortDescending is true then assocProducts will be returned sorted + by sequenceNum descending (defaults to false). + </description> <attribute name="productId" type="String" mode="IN" optional="true"/> <attribute name="productIdTo" type="String" mode="IN" optional="true"/> <attribute name="checkViewAllow" type="Boolean" mode="IN" optional="true"/> <attribute name="prodCatalogId" type="String" mode="IN" optional="true"/> <attribute name="type" type="String" mode="IN"/> + <attribute name="bidirectional" type="Boolean" mode="IN" optional="true"/> + <attribute name="sortDescending" type="Boolean" mode="IN" optional="true"/> <attribute name="assocProducts" type="java.util.Collection" mode="OUT" optional="true"/> </service> <service name="getProductFeatures" engine="java" Modified: ofbiz/trunk/applications/product/src/org/ofbiz/product/product/ProductServices.java URL: http://svn.apache.org/viewvc/ofbiz/trunk/applications/product/src/org/ofbiz/product/product/ProductServices.java?rev=798500&r1=798499&r2=798500&view=diff ============================================================================== --- ofbiz/trunk/applications/product/src/org/ofbiz/product/product/ProductServices.java (original) +++ ofbiz/trunk/applications/product/src/org/ofbiz/product/product/ProductServices.java Tue Jul 28 12:13:28 2009 @@ -43,6 +43,8 @@ import org.ofbiz.entity.GenericDelegator; import org.ofbiz.entity.GenericEntityException; import org.ofbiz.entity.GenericValue; +import org.ofbiz.entity.condition.EntityCondition; +import org.ofbiz.entity.condition.EntityJoinOperator; import org.ofbiz.entity.util.EntityUtil; import org.ofbiz.product.image.ScaleImage; import org.ofbiz.product.catalog.CatalogWorker; @@ -423,8 +425,12 @@ String errMsg = null; Boolean cvaBool = (Boolean) context.get("checkViewAllow"); - boolean checkViewAllow = (cvaBool == null ? false : cvaBool.booleanValue()); + boolean checkViewAllow = (cvaBool == null ? false : cvaBool); String prodCatalogId = (String) context.get("prodCatalogId"); + Boolean bidirectional = (Boolean) context.get("bidirectional"); + bidirectional = bidirectional == null ? false : bidirectional; + Boolean sortDescending = (Boolean) context.get("sortDescending"); + sortDescending = sortDescending == null ? false : sortDescending; if (productId == null && productIdTo == null) { errMsg = UtilProperties.getMessage(resource,"productservices.both_productId_and_productIdTo_cannot_be_null", locale); @@ -462,11 +468,28 @@ try { List<GenericValue> productAssocs = null; + + List<String> orderBy = FastList.newInstance(); + if (sortDescending) { + orderBy.add("sequenceNum DESC"); + } else { + orderBy.add("sequenceNum"); + } - if (productIdTo == null) { - productAssocs = product.getRelatedCache("MainProductAssoc", UtilMisc.toMap("productAssocTypeId", type), UtilMisc.toList("sequenceNum")); + if (bidirectional) { + EntityCondition cond = EntityCondition.makeCondition( + UtilMisc.toList( + EntityCondition.makeCondition("productId", productId), + EntityCondition.makeCondition("productIdTo", productId) + ), EntityJoinOperator.OR); + cond = EntityCondition.makeCondition(cond, EntityCondition.makeCondition("productAssocTypeId", type)); + productAssocs = delegator.findList("ProductAssoc", cond, null, orderBy, null, true); } else { - productAssocs = product.getRelatedCache("AssocProductAssoc", UtilMisc.toMap("productAssocTypeId", type), UtilMisc.toList("sequenceNum")); + if (productIdTo == null) { + productAssocs = product.getRelatedCache("MainProductAssoc", UtilMisc.toMap("productAssocTypeId", type), orderBy); + } else { + productAssocs = product.getRelatedCache("AssocProductAssoc", UtilMisc.toMap("productAssocTypeId", type), orderBy); + } } // filter the list by date productAssocs = EntityUtil.filterByDate(productAssocs); Modified: ofbiz/trunk/specialpurpose/ecommerce/webapp/ecommerce/catalog/productdetail.ftl URL: http://svn.apache.org/viewvc/ofbiz/trunk/specialpurpose/ecommerce/webapp/ecommerce/catalog/productdetail.ftl?rev=798500&r1=798499&r2=798500&view=diff ============================================================================== --- ofbiz/trunk/specialpurpose/ecommerce/webapp/ecommerce/catalog/productdetail.ftl (original) +++ ofbiz/trunk/specialpurpose/ecommerce/webapp/ecommerce/catalog/productdetail.ftl Tue Jul 28 12:13:28 2009 @@ -696,13 +696,20 @@ <div class="productsummary-container"> <#list assocProducts as productAssoc> + <#if productAssoc.productId == product.productId> + <#assign assocProductId = productAssoc.productIdTo/> + <#else/> + <#assign assocProductId = productAssoc.productId/> + </#if> <div> - <a href="<@ofbizUrl>${targetRequest}/<#if categoryId?exists>~category_id=${categoryId}/</#if>~product_id=${productAssoc.productIdTo?if_exists}</@ofbizUrl>" class="buttontext"> - ${productAssoc.productIdTo?if_exists} + <a href="<@ofbizUrl>${targetRequest}/<#if categoryId?exists>~category_id=${categoryId}/</#if>~product_id=${assocProductId}</@ofbizUrl>" class="buttontext"> + ${assocProductId} </a> - - <b>${productAssoc.reason?if_exists}</b> + <#if productAssoc.reason?has_content> + - <b>${productAssoc.reason}</b> + </#if> </div> - ${setRequestAttribute("optProductId", productAssoc.productIdTo)} + ${setRequestAttribute("optProductId", assocProductId)} ${setRequestAttribute("listIndex", listIndex)} ${setRequestAttribute("formNamePrefix", formNamePrefix)} <#if targetRequestName?has_content> @@ -723,6 +730,8 @@ <#assign listIndex = 1> ${setRequestAttribute("productValue", productValue)} <div id="associated-products"> + <#-- also bought --> + <@associated assocProducts=alsoBoughtProducts beforeName="" showName="N" afterName="${uiLabelMap.ProductAlsoBought}" formNamePrefix="albt" targetRequestName=""/> <#-- obsolete --> <@associated assocProducts=obsoleteProducts beforeName="" showName="Y" afterName=" ${uiLabelMap.ProductObsolete}" formNamePrefix="obs" targetRequestName=""/> <#-- cross sell --> |
Free forum by Nabble | Edit this page |