Skip to content

Commit

Permalink
Merge pull request #16 from hotwax/15-facility-group-filter-in-config…
Browse files Browse the repository at this point in the history
…uring-the-brokering-rules

Added support to filter order by facility group
  • Loading branch information
dixitdeepak authored May 16, 2024
2 parents 1da337a + 7918d78 commit 5fca345
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 7 deletions.
3 changes: 3 additions & 0 deletions data/OrderRoutingSeedData.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
<org.apache.ofbiz.common.enum.Enumeration enumId="OIP_PRIORITY" description="Order Priority" sequenceNum="20" enumTypeId="ORD_FILTER_PRM_TYPE" enumCode="priority"/>
<org.apache.ofbiz.common.enum.Enumeration enumId="OIP_PROMISE_DATE" description="Promise Date" sequenceNum="25" enumTypeId="ORD_FILTER_PRM_TYPE" enumCode="promiseDaysCutoff"/>
<org.apache.ofbiz.common.enum.Enumeration enumId="OIP_SALES_CHANNEL" description="Sales channel" sequenceNum="5" enumTypeId="ORD_FILTER_PRM_TYPE" enumCode="salesChannelEnumId"/>
<org.apache.ofbiz.common.enum.Enumeration enumId="OIP_ORIGIN_FAC_GRP" description="Origin facility group" sequenceNum="30" enumTypeId="ORD_FILTER_PRM_TYPE" enumCode="originFacilityGroupId"/>
<org.apache.ofbiz.common.enum.Enumeration enumId="OIP_PROD_CATEGORY" description="Product category" sequenceNum="35" enumTypeId="ORD_FILTER_PRM_TYPE" enumCode="productCategoryId"/>

<org.apache.ofbiz.common.enum.EnumerationType enumTypeId="INV_FILTER_PRM_TYPE" description="Determine the input parameter or field to be considered in a inventory condition " parentTypeId="ORDER_ROUTING"/>
<org.apache.ofbiz.common.enum.Enumeration enumId="IIP_FACILITY_GROUP" description="Facility group" sequenceNum="5" enumTypeId="INV_FILTER_PRM_TYPE" enumCode="facilityGroupId"/>
Expand All @@ -44,6 +46,7 @@
<org.apache.ofbiz.common.enum.Enumeration enumId="OSP_SHIP_AFTER" description="Ship after" sequenceNum="10" enumTypeId="ORD_SORT_PARAM_TYPE" enumCode="shipAfterDate"/><!-- OrderItem.shipAfterDate -->
<org.apache.ofbiz.common.enum.Enumeration enumId="OSP_ORDER_DATE" description="Order date" sequenceNum="15" enumTypeId="ORD_SORT_PARAM_TYPE" enumCode="orderDate"/><!-- OrderHeader.orderDate -->
<org.apache.ofbiz.common.enum.Enumeration enumId="OSP_SHIP_METH" description="Shipping method" sequenceNum="20" enumTypeId="ORD_SORT_PARAM_TYPE" enumCode="deliveryDays"/> <!-- CarrierShipmentMethod.deliveryDays -->
<org.apache.ofbiz.common.enum.Enumeration enumId="OSP_PRIORITY" description="Order priority" sequenceNum="25" enumTypeId="ORD_SORT_PARAM_TYPE" enumCode="priority"/>

<org.apache.ofbiz.common.enum.EnumerationType enumTypeId="INV_SORT_PARAM_TYPE" description="Determine the order by parameter considered in a condition" parentTypeId="ORDER_ROUTING"/>
<org.apache.ofbiz.common.enum.Enumeration enumId="ISP_PROXIMITY" description="Proximity" sequenceNum="5" enumTypeId="INV_SORT_PARAM_TYPE" enumCode="distance"/>
Expand Down
4 changes: 2 additions & 2 deletions screen/OrderRoutingGroup/OrderRouting/OrderRoutingDetail.xml
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@
</entity-options>
</drop-down>
</default-field></field>
<field name="fieldValue"><default-field><display/></default-field></field>
<field name="sequenceNum"><default-field><display/></default-field></field>
<field name="fieldValue"><default-field><text-line/></default-field></field>
<field name="sequenceNum"><default-field><text-line/></default-field></field>
<field name="createdDate"><default-field><hidden default-value="${ec.user.nowTimestamp}"/></default-field></field>
<field name="submitButton"><default-field title="Add"><submit/></default-field></field>
</form-single>
Expand Down
139 changes: 138 additions & 1 deletion service/co/hotwax/order/routing/OrderRoutingServices.xml
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@
</actions>
</service>

<service verb="run" noun="OrderRouting" transaction-timeout="36000">
<service verb="run" noun="OldOrderRouting" transaction-timeout="36000">
<description>
The service is to streamline the fulfillment process by intelligently routing orders through the system based on predefined conditions and rules.
This service aims to optimize inventory distribution and order fulfillment efficiency by dynamically applying routing rules to each order's ship group,
Expand Down Expand Up @@ -285,6 +285,143 @@
</actions>
</service>

<service verb="run" noun="OrderRouting" transaction-timeout="36000">
<description>
The service is to streamline the fulfillment process by intelligently routing orders through the system based on predefined conditions and rules.
This service aims to optimize inventory distribution and order fulfillment efficiency by dynamically applying routing rules to each order's ship group,
ensuring that inventory allocation and subsequent actions are aligned with business logistics and inventory management strategies.
</description>
<in-parameters>
<parameter name="orderRoutingId" required="true"/>
<parameter name="orderId"/>
<parameter name="shipGroupSeqId"/>
</in-parameters>
<actions>
<entity-find-one entity-name="co.hotwax.order.routing.OrderRouting" value-field="orderRouting" cache="true"/>
<if condition="!orderRouting">
<return error="true" message="No order routing found for id ${orderRoutingId}"/>
</if>
<if condition="!'ROUTING_ACTIVE'.equals(orderRouting.statusId)">
<return error="true" message="Order routing ${orderRouting.routingName} [${orderRoutingId}] is not active"/>
</if>
<entity-find-related-one value-field="orderRouting" relationship-name="co.hotwax.order.routing.OrderRoutingGroup" to-value-field="orderRoutingGroup" cache="true"/>
<log message="Started order routing ${orderRouting.routingName} [${orderRoutingId}]"/>

<entity-find entity-name="co.hotwax.order.routing.OrderFilterCondition" list="orderFilterConditions">
<econdition field-name="orderRoutingId" from="orderRoutingId"/>
<order-by field-name="sequenceNum"/>
<order-by field-name="createdDate DESC"/>
</entity-find>
<!-- Prepare the filter conditions for the order routing -->
<filter-map-list list="orderFilterConditions" to-list="filterConditions">
<field-map field-name="conditionTypeEnumId" value="ENTCT_FILTER"/>
</filter-map-list>
<!-- Prepare the order by for the order routing -->
<filter-map-list list="orderFilterConditions" to-list="sortFields">
<field-map field-name="conditionTypeEnumId" value="ENTCT_SORT_BY"/>
</filter-map-list>
<set field="orderSortByList" from="sortFields != null ? sortFields.fieldName:[]" type="List"/>
<entity-find entity-name="co.hotwax.order.routing.OrderRoutingRule" list="orderRoutingRules">
<econditions>
<econdition field-name="orderRoutingId" from="orderRoutingId"/>
<econdition field-name="statusId" value="RULE_ACTIVE"/>
</econditions>
<order-by field-name="sequenceNum"/>
</entity-find>
<if condition="!orderRoutingRules">
<return error="true" message="No routing rule setup for order routing ${orderRouting.routingName} [${orderRoutingId}]"/>
</if>

<set field="orderTypeId" value="SALES_ORDER"/>
<set field="productStoreId" from="orderRoutingGroup.productStoreId"/>
<set field="itemStatusId" value="ITEM_APPROVED"/>
<set field="orderStatusId" value="ORDER_APPROVED"/>
<set field="facilityParentTypeId" value="VIRTUAL_FACILITY"/>
<set field="selectOrderItemSeqId" value="false" type="Boolean"/>
<set field="orderFilterConditions" from="[]"/>
<set field="attemptedCount" value="0" type="Integer"/>
<iterate list="filterConditions" entry="filterCondition">
<if condition='"promiseDaysCutoff".equals(filterCondition.fieldName)'>
<if condition="filterCondition.fieldValue">
<!--
When promiseDaysCutoff is configured, it acts as a constraint that prioritizes order items to be processed and allocated in a way that meets the delivery promise timeframe.
The brokering process is executed at the individual order item level rather than at a more aggregated shipGroup level.
-->
<set field="selectOrderItemSeqId" value="true" type="Boolean"/>
<script>
promisedDatetime = java.time.ZonedDateTime.now().plusDays(filterCondition.fieldValue as Long)
.with(java.time.LocalTime.MAX).format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))
</script>
<set field="orderFilterConditions" from="orderFilterConditions + [fieldName: 'promisedDatetime', operator: filterCondition.operator, fieldValue: promisedDatetime]"/>
</if>
<else-if condition='"originFacilityGroupId".equals(filterCondition.fieldName)'>
<set field="facilityGroupCondition" from="filterCondition"/>
</else-if>
<else-if condition='"productCategoryId".equals(filterCondition.fieldName)'>
<set field="productCategoryCondition" from="filterCondition"/>
</else-if>
<else>
<set field="orderFilterConditions" from="orderFilterConditions + filterCondition"/>
</else>
</if>
</iterate>
<set field="templateLoc" value="component://order-routing/sql/EligibleOrdersQuery.sql.ftl"/>
<log message="Fetching order with condition ${orderFilterConditions} for routing ${orderRouting.routingName} [${orderRoutingId}]"/>
<script><![CDATA[
Writer writer = new StringWriter()
ec.resourceFacade.template(templateLoc, writer)
//ec.logger.info("Eligible orders sql: ${writer}")
/** Note:
We can use statement.executeQuery as well.
However, using `requireNewTransaction` causes issues with `ResultSet` objects.
When `requireNewTransaction` is used, the `ResultSet` gets closed prematurely.
*/
def fieldList =['orderId', 'shipGroupSeqId'];
if (selectOrderItemSeqId) {
fieldList.add('orderItemSeqId')
}
try (eli = ec.entityFacade.sqlFind(writer.toString(), null, "co.hotwax.order.OrderItemsQueue", fieldList)) {
EntityValue nextValue
while ((nextValue = (EntityValue) eli.next()) != null) {
attemptedCount++;
def orderItemSeqId = null;
if (selectOrderItemSeqId) {
orderItemSeqId = nextValue.orderItemSeqId
}
ruleIterator = orderRoutingRules.iterator()
while (ruleIterator.hasNext()) {
def routingRule = ruleIterator.next()
ruleResult = ec.service.sync().name("co.hotwax.order.routing.OrderRoutingServices.run#OrderRoutingRule")
.parameters([routingRuleId: routingRule.routingRuleId,
orderId: nextValue.orderId, shipGroupSeqId: nextValue.shipGroupSeqId, orderItemSeqId: orderItemSeqId])
.requireNewTransaction(true)
.call()
if (!ec.message.hasError()) {
actionResult = ec.service.sync().name("co.hotwax.order.routing.OrderRoutingServices.eval#OrderRoutingActions")
.parameters([orderId: nextValue.orderId, shipGroupSeqId: nextValue.shipGroupSeqId,orderItemSeqId: orderItemSeqId,
routingRuleId: routingRule.routingRuleId, suggestedFulfillmentLocations: ruleResult.suggestedFulfillmentLocations])
.requireNewTransaction(true)
.call()
if (ec.message.hasError()) {
ec.logger.warn("Ignoring order routing actions errors for ${routingRule.ruleName} [${routingRule.routingRuleId}] " + ec.message.getErrorsString())
ec.message.clearAll()
}
if (!actionResult?.runNextRule) {
/* If an order is routed to a facility, all subsequent order routing rule executions should be excluded */
break
}
} else {
ec.logger.warn("Ignoring order routing rule errors " + ec.message.getErrorsString())
ec.message.clearAll()
}
}
}
}
]]></script>
<return message="Attempted ${attemptedCount} ${selectOrderItemSeqId?'item(s)':'ship group(s)'} for the '${orderRouting.routingName} [${orderRoutingId}] order routing"/>
</actions>
</service>

<service verb="run" noun="OrderRoutingRule" transaction-timeout="36000">
<description>
To dynamically fetch and allocate inventory for order fulfillment based on specific routing rule configurations.
Expand Down
65 changes: 65 additions & 0 deletions sql/EligibleOrdersQuery.sql.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<#macro buildSqlCondition fieldName filterCodnition>
${fieldName} ${Static["co.hotwax.order.routing.OrderRoutingHelper"].makeSqlWhere(filterCodnition)!}
</#macro>
SELECT
ORDER_ID'orderId',
SHIP_GROUP_SEQ_ID'shipGroupSeqId'
<#if selectOrderItemSeqId>,ORDER_ITEM_SEQ_ID'orderItemSeqId' <#--conditional field to select --></#if>
from
(SELECT
OH.ORDER_ID,
OIS.SHIP_GROUP_SEQ_ID,
OI.ORDER_ITEM_SEQ_ID,
OH.PRODUCT_STORE_ID,
OH.STATUS_ID,
OH.ORDER_TYPE_ID,
OI.STATUS_ID AS ITEM_STATUS_ID,
FACTYPE.PARENT_TYPE_ID AS FACILITY_PARENT_TYPE_ID,
OH.SALES_CHANNEL_ENUM_ID AS salesChannelEnumId,
OI.PROMISED_DATETIME AS promisedDatetime,
OIS.FACILITY_ID AS facilityId,
OIS.SHIPMENT_METHOD_TYPE_ID as shipmentMethodTypeId,
OH.PRIORITY as priority,
OIS.SHIP_AFTER_DATE AS shipAfterDate,
OH.ORDER_DATE AS orderDate,
CSM.DELIVERY_DAYS AS deliveryDays
FROM
ORDER_HEADER OH
INNER JOIN ORDER_ITEM_SHIP_GROUP OIS ON OH.ORDER_ID = OIS.ORDER_ID
INNER JOIN ORDER_ITEM OI ON OIS.ORDER_ID = OI.ORDER_ID AND OIS.SHIP_GROUP_SEQ_ID = OI.SHIP_GROUP_SEQ_ID
<#if productCategoryCondition?has_content>INNER JOIN PRODUCT_CATEGORY_MEMBER PCM ON PCM.PRODUCT_ID=OI.PRODUCT_ID AND <@buildSqlCondition 'PCM.PRODUCT_CATEGORY_ID' productCategoryCondition/></#if>
<#if facilityGroupCondition?has_content>INNER JOIN FACILITY_GROUP_MEMBER FGM ON FGM.FACILITY_ID=OH.ORIGIN_FACILITY_ID AND <@buildSqlCondition 'FGM.FACILITY_GROUP_ID' facilityGroupCondition/></#if>
LEFT OUTER JOIN CARRIER_SHIPMENT_METHOD CSM ON OIS.SHIPMENT_METHOD_TYPE_ID = CSM.SHIPMENT_METHOD_TYPE_ID AND OIS.CARRIER_PARTY_ID = CSM.PARTY_ID AND OIS.CARRIER_ROLE_TYPE_ID = CSM.ROLE_TYPE_ID
LEFT OUTER JOIN FACILITY FAC ON OIS.FACILITY_ID = FAC.FACILITY_ID
LEFT OUTER JOIN FACILITY_TYPE FACTYPE ON FAC.FACILITY_TYPE_ID = FACTYPE.FACILITY_TYPE_ID) as ORD
WHERE
(
PRODUCT_STORE_ID = '${productStoreId!''}'
AND STATUS_ID = '${orderStatusId!''}'
AND ORDER_TYPE_ID = '${orderTypeId!''}'
AND ITEM_STATUS_ID = '${itemStatusId!''}'
AND FACILITY_PARENT_TYPE_ID = '${facilityParentTypeId!''}'
<#if orderId?has_content>
AND orderId= '${orderId}'
<#if shipGroupSeqId?has_content> AND shipGroupSeqId = ${shipGroupSeqId}</#if>
</#if>
<#if orderFilterConditions?has_content>
<#list orderFilterConditions as filterCondition>
AND <@buildSqlCondition filterCondition.fieldName filterCondition/>
</#list>
</#if>
)
GROUP BY
orderId,
shipGroupSeqId
<#if selectOrderItemSeqId>, orderItemSeqId</#if> <#-- If items are filtered using promisedDatetime, we need to include orderItemSeqId in the GROUP BY clause -->
HAVING
COUNT(ORDER_ITEM_SEQ_ID) > '0'
ORDER BY
<#if orderSortByList?has_content>
<#list orderSortByList as orderSortBy>
${orderSortBy!}<#sep>,
</#list>
<#else>
orderDate ASC
</#if>
2 changes: 1 addition & 1 deletion sql/InventorySourceSelector.sql.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ x.*
AND ((ifnull(foc.last_order_count,0) +1 < f.maximum_order_limit) OR f.maximum_order_limit is null)
<#if invenoryGroupFiter?has_content>AND fgm.FACILITY_GROUP_ID <@buildSqlCondition value=invenoryGroupFiter /></#if> -- in ('${(invenoryGroupFiter.get("fieldValue"))!}') -- NEW facility group ids need to be passed for the groups on which routing is expected to be performed
having
<#if distance?has_content>distance <@buildSqlCondition value=distance /> and </#if> -- >= ${(distance.get("fieldValue"))!0} -- TODO: Need to check for miles/km
<#if distance?has_content>distance <@buildSqlCondition value=distance /> and </#if> -- >= ${(distance.get("fieldValue"))!0}
<#if !orderRoutingRule.assignmentEnumId?has_content || 'ORA_SINGLE' == orderRoutingRule.assignmentEnumId> rank_by_order_above_facility_threshold='Y' -- NEW for sorting facility having all the items above threshold
<#elseif 'ORA_MULTI' == orderRoutingRule.assignmentEnumId> case when rank_by_order_at_facility='Y' then item_at_facility_above_threshold='Y' end </#if>
order by
Expand Down
10 changes: 7 additions & 3 deletions src/main/java/co/hotwax/order/routing/OrderRoutingHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.stream.Collectors;

public class OrderRoutingHelper {
protected static final Logger logger = LoggerFactory.getLogger(OrderRoutingHelper.class);
protected static Cache<String, String> jwtCache;
public static String makeSqlWhere(EntityValue ev) {
public static String makeSqlWhere(Map<String, Object> ev) {
@SuppressWarnings("MismatchedQueryAndUpdateOfStringBuilder")
StringBuilder sql = new StringBuilder();
boolean valueDone = false;
Object value = ev.get("fieldValue");
EntityCondition.ComparisonOperator operator = EntityConditionFactoryImpl.getComparisonOperator(ev.getString("operator"));
EntityCondition.ComparisonOperator operator = EntityConditionFactoryImpl.getComparisonOperator((String) ev.get("operator"));
if (value == null) {
if (operator == EntityCondition.EQUALS || operator == EntityCondition.LIKE || operator == EntityCondition.IN || operator == EntityCondition.BETWEEN) {
sql.append(" IS NULL");
Expand Down Expand Up @@ -67,7 +69,9 @@ static Object valueToCollection(Object value) {
if (value instanceof CharSequence) {
String valueStr = value.toString();
// note: used to do this, now always put in List: if (valueStr.contains(","))
value = Arrays.asList(valueStr.split(","));
value = Arrays.stream(valueStr.split(","))
.map(String::trim) // Trim each element
.collect(Collectors.toList());
}
// TODO: any other useful types to convert?
return value;
Expand Down

0 comments on commit 5fca345

Please sign in to comment.