This is an automated email from the ASF dual-hosted git repository.
diveshdut pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new d711a69722 OFBIZ-13452: Use facility shipping calendars for
SALES_ORDER_SHIP MRP scheduling
d711a69722 is described below
commit d711a69722c9a80992c89239567dddc88f13b56a
Author: Divesh Dutta <[email protected]>
AuthorDate: Tue Jul 7 15:54:09 2026 +0530
OFBIZ-13452: Use facility shipping calendars for SALES_ORDER_SHIP MRP
scheduling
## Summary
This change makes purchased-item MRP backward scheduling calendar-aware
for the `SALES_ORDER_SHIP` path by resolving
`ProductFacility.daysToShip` against facility-specific shipping
calendars instead of the global `SUPPLIER` calendar.
It also keeps supplier lead time additive for this sales-order-driven
replenishment path, so MRP can schedule both:
- the seller facility's final shipping/handling leg
- the upstream supplier replenishment lead time
## What changed
- preserve `ProductFacility.daysToShip` separately from supplier lead
time in `MrpServices`
- pass `mrpEventTypeId`, facility shipping days, and supplier lead time
into `ProposedOrder.calculateStartDate(...)`
- apply facility calendar logic only for `SALES_ORDER_SHIP`
- resolve facility calendars through `FacilityCalendar` filtered by
`facilityCalendarTypeId="SHIPPING"`
- fall back to global `DEFAULT` `TechDataCalendar`, then to direct day
arithmetic
- apply supplier lead time only for purchased replenishment driven by
`SALES_ORDER_SHIP`
- add seed data for:
- `FacilityCalendarType.SHIPPING`
- `TechDataCalendarWeek` / `TechDataCalendar` with id `SHIPPING`
- add demo data assigning `WebStoreWarehouse` to the `SHIPPING` facility
calendar
## Business intent
For this ticket, `ProductFacility.daysToShip` is treated as the seller
facility's own shipping/handling time.
`SALES_ORDER_SHIP` is the downstream demand signal. When MRP needs to
propose purchased replenishment for that demand, supplier lead time is
applied as the upstream replenishment leg, while facility shipping time
is applied as the final seller-controlled leg.
## Scope
Included:
- `SALES_ORDER_SHIP` purchased-item backward scheduling
- facility-specific shipping calendar lookup
- additive scheduling of facility shipping time and supplier lead time
for this path
---
applications/datamodel/data/demo/OrderDemoData.xml | 1 +
.../datamodel/data/seed/ManufacturingSeedData.xml | 2 +
.../datamodel/data/seed/ProductSeedData.xml | 1 +
.../apache/ofbiz/manufacturing/bom/BOMNode.java | 3 +-
.../ofbiz/manufacturing/mrp/MrpServices.java | 30 ++++++----
.../ofbiz/manufacturing/mrp/ProposedOrder.java | 64 +++++++++++++++++-----
6 files changed, 75 insertions(+), 26 deletions(-)
diff --git a/applications/datamodel/data/demo/OrderDemoData.xml
b/applications/datamodel/data/demo/OrderDemoData.xml
index 0a2b960475..81af2ff9da 100644
--- a/applications/datamodel/data/demo/OrderDemoData.xml
+++ b/applications/datamodel/data/demo/OrderDemoData.xml
@@ -1212,6 +1212,7 @@ under the License.
<ProductCategoryRollup fromDate="2001-05-13 12:00:00.0"
parentProductCategoryId="2011" productCategoryId="20111"/>
<!-- some default product facility settings -->
+ <FacilityCalendar facilityId="WebStoreWarehouse" calendarId="SHIPPING"
facilityCalendarTypeId="SHIPPING" fromDate="2001-05-13 12:00:00.0"/>
<ProductFacility productId="GZ-2644" facilityId="WebStoreWarehouse"
minimumStock="2" reorderQuantity="10" daysToShip="15"/>
<ProductFacility productId="GZ-8544" facilityId="WebStoreWarehouse"
minimumStock="20" reorderQuantity="50" daysToShip="1"/>
<ProductFacility productId="WG-1111" facilityId="WebStoreWarehouse"
minimumStock="2" reorderQuantity="10" daysToShip="1"/>
diff --git a/applications/datamodel/data/seed/ManufacturingSeedData.xml
b/applications/datamodel/data/seed/ManufacturingSeedData.xml
index 0a06a800f5..fd695a282c 100644
--- a/applications/datamodel/data/seed/ManufacturingSeedData.xml
+++ b/applications/datamodel/data/seed/ManufacturingSeedData.xml
@@ -35,6 +35,8 @@ under the License.
<TechDataCalendarWeek calendarWeekId="SUPPLIER" description="8hours/days,
currently the Re-Order Process convert day to mms with 8h/days"
mondayStartTime="08:30:00" mondayCapacity="2.88E7" tuesdayStartTime="08:30:00"
tuesdayCapacity="2.88E7" wednesdayStartTime="08:30:00"
wednesdayCapacity="2.88E7" thursdayStartTime="08:30:00"
thursdayCapacity="2.88E7" fridayStartTime="08:30:00" fridayCapacity="2.88E7"/>
<TechDataCalendar calendarId="SUPPLIER" description="Calendar used for
Re-Order date calculation for bought product" calendarWeekId="SUPPLIER"/>
+ <TechDataCalendarWeek calendarWeekId="SHIPPING" description="8hours/days
working weekdays, used for facility daysToShip calculation"
mondayStartTime="08:30:00" mondayCapacity="2.88E7" tuesdayStartTime="08:30:00"
tuesdayCapacity="2.88E7" wednesdayStartTime="08:30:00"
wednesdayCapacity="2.88E7" thursdayStartTime="08:30:00"
thursdayCapacity="2.88E7" fridayStartTime="08:30:00" fridayCapacity="2.88E7"/>
+ <TechDataCalendar calendarId="SHIPPING" description="Calendar used for
facility daysToShip date calculation (working weekdays)"
calendarWeekId="SHIPPING"/>
<TechDataCalendarWeek calendarWeekId="DEFAULT" description="8hours/days"
mondayStartTime="08:30:00" mondayCapacity="2.88E7" tuesdayStartTime="08:30:00"
tuesdayCapacity="2.88E7" wednesdayStartTime="08:30:00"
wednesdayCapacity="2.88E7" thursdayStartTime="08:30:00"
thursdayCapacity="2.88E7" fridayStartTime="08:30:00" fridayCapacity="2.88E7"/>
<TechDataCalendar calendarId="DEFAULT" description="Default calendar used
when no specific calendar is defined" calendarWeekId="DEFAULT"/>
diff --git a/applications/datamodel/data/seed/ProductSeedData.xml
b/applications/datamodel/data/seed/ProductSeedData.xml
index ee9d6cb219..fd902ccfa9 100644
--- a/applications/datamodel/data/seed/ProductSeedData.xml
+++ b/applications/datamodel/data/seed/ProductSeedData.xml
@@ -425,6 +425,7 @@ under the License.
<FacilityType description="Room" facilityTypeId="ROOM" hasTable="N"/>
<FacilityType description="Retail Store" facilityTypeId="RETAIL_STORE"
hasTable="N"/>
<FacilityType description="Warehouse" facilityTypeId="WAREHOUSE"
hasTable="N"/>
+ <FacilityCalendarType facilityCalendarTypeId="SHIPPING"
description="Shipping/handling working calendar"/>
<FacilityAssocType facilityAssocTypeId="BACKUP_WAREHOUSE"
description="Facility that serves another facility in terms of inventory"/>
diff --git
a/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/bom/BOMNode.java
b/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/bom/BOMNode.java
index 44ee6e7843..28ae89635a 100644
---
a/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/bom/BOMNode.java
+++
b/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/bom/BOMNode.java
@@ -656,7 +656,7 @@ public class BOMNode {
Timestamp minStartDate = requiredBydate;
if ("WIP".equals(getProduct().getString("productTypeId")) || allNodes)
{
ProposedOrder proposedOrder = new ProposedOrder(getProduct(),
facilityId, facilityId, true, requiredBydate, getQuantity());
- proposedOrder.calculateStartDate(0, null, delegator, dispatcher,
userLogin);
+ proposedOrder.calculateStartDate(null, 0, 0, null, delegator,
dispatcher, userLogin);
Timestamp startDate = proposedOrder.getRequirementStartDate();
minStartDate = startDate;
for (BOMNode oneChildNode : childrenNodes) {
@@ -929,4 +929,3 @@ public class BOMNode {
}
}
-
diff --git
a/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/mrp/MrpServices.java
b/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/mrp/MrpServices.java
index 50d4d6d276..574d41f47c 100644
---
a/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/mrp/MrpServices.java
+++
b/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/mrp/MrpServices.java
@@ -60,6 +60,14 @@ public class MrpServices {
private static final String RESOURCE = "ManufacturingUiLabels";
private static final String MAIN_SUPPLIER_PREF_ORDER_ID = "10_MAIN_SUPPL";
+ private static boolean shouldApplyFacilityDaysToShip(String
mrpEventTypeId) {
+ return "SALES_ORDER_SHIP".equals(mrpEventTypeId);
+ }
+
+ private static boolean shouldApplySupplierLeadTime(String mrpEventTypeId,
boolean isBuilt) {
+ return !isBuilt && "SALES_ORDER_SHIP".equals(mrpEventTypeId);
+ }
+
public static Map<String, Object> initMrpEvents(DispatchContext ctx,
Map<String, ? extends Object> context) {
Delegator delegator = ctx.getDelegator();
LocalDispatcher dispatcher = ctx.getDispatcher();
@@ -710,7 +718,6 @@ public class MrpServices {
Timestamp eventDate = null;
BigDecimal reorderQuantity = BigDecimal.ZERO;
BigDecimal minimumStock = BigDecimal.ZERO;
- int daysToShip = 0;
List<BOMNode> components = null;
boolean isBuilt = false;
GenericValue routing = null;
@@ -794,15 +801,11 @@ public class MrpServices {
// days to ship is only relevant for sales order to
plan for preparatory days to ship. Otherwise MRP will push event dates
// for manufacturing parts
// as well and cause problems
- daysToShip = 0;
if (productFacility != null) {
reorderQuantity =
(productFacility.getBigDecimal("reorderQuantity") != null ?
productFacility.getBigDecimal(
"reorderQuantity") :
BigDecimal.ONE.negate());
minimumStock =
(productFacility.getBigDecimal("minimumStock") != null ?
productFacility.getBigDecimal("minimumStock")
: BigDecimal.ZERO);
- if
("SALES_ORDER_SHIP".equals(inventoryEventForMRP.getString("mrpEventTypeId"))) {
- daysToShip =
(productFacility.getLong("daysToShip") != null ?
productFacility.getLong("daysToShip").intValue() : 0);
- }
} else {
minimumStock = BigDecimal.ZERO;
reorderQuantity = BigDecimal.ONE.negate();
@@ -880,10 +883,16 @@ public class MrpServices {
}
//
#####################################################
- int startDateOffsetDays = daysToShip;
- if (!isBuilt) {
+ String mrpEventTypeId =
inventoryEventForMRP.getString("mrpEventTypeId");
+ int facilityDaysToShip = 0;
+ if (productFacility != null &&
shouldApplyFacilityDaysToShip(mrpEventTypeId)) {
+ facilityDaysToShip =
(productFacility.getLong("daysToShip") != null
+ ?
productFacility.getLong("daysToShip").intValue() : 0);
+ }
+ int supplierLeadTimeDays = 0;
+ if (shouldApplySupplierLeadTime(mrpEventTypeId,
isBuilt)) {
try {
- startDateOffsetDays =
getSupplierProductLeadTimeDays(delegator, product.getString("productId"),
eventDate);
+ supplierLeadTimeDays =
getSupplierProductLeadTimeDays(delegator, product.getString("productId"),
eventDate);
} catch (GenericEntityException e) {
return
ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE,
"ManufacturingMrpCannotFindProductForEvent",
locale));
@@ -891,9 +900,8 @@ public class MrpServices {
}
// calculate the ProposedOrder requirementStartDate
and update the requirementStartDate object property.
- Map<String, Object> routingTaskStartDate =
proposedOrder.calculateStartDate(startDateOffsetDays, routing, delegator,
- dispatcher,
- userLogin);
+ Map<String, Object> routingTaskStartDate =
proposedOrder.calculateStartDate(mrpEventTypeId, facilityDaysToShip,
+ supplierLeadTimeDays, routing, delegator,
dispatcher, userLogin);
if (isBuilt) {
// process the product components
processBomComponent(mrpId, product,
proposedOrder.getQuantity(), proposedOrder.getRequirementStartDate(),
diff --git
a/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/mrp/ProposedOrder.java
b/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/mrp/ProposedOrder.java
index 896841ab72..7311c12f76 100644
---
a/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/mrp/ProposedOrder.java
+++
b/applications/manufacturing/src/main/java/org/apache/ofbiz/manufacturing/mrp/ProposedOrder.java
@@ -100,7 +100,12 @@ public class ProposedOrder {
* <li> step by step calculate from the endDate the startDate</li>
* </ul>
* </li>
- * <li>For the bought product, the first ProductFacility.daysToShip is
used to calculated the startDate</li>
+ * <li>For purchased products created from SALES_ORDER_SHIP demand,
supplier lead time is applied using the
+ * SUPPLIER calendar when available.</li>
+ * <li>For SALES_ORDER_SHIP purchased products,
ProductFacility.daysToShip is first applied as the seller
+ * facility's final shipping/handling leg using the active SHIPPING
facility calendar, falling back to the
+ * DEFAULT calendar and then direct day arithmetic, and supplier
lead time is then applied before that
+ * facility-ready date as the upstream replenishment leg.</li>
* </ul>
* @return
* <ul>
@@ -108,12 +113,12 @@ public class ProposedOrder {
* <li>else null.</li>
* </ul>
**/
- public Map<String, Object> calculateStartDate(int daysToShip, GenericValue
routing, Delegator delegator, LocalDispatcher dispatcher,
- GenericValue userLogin) {
+ public Map<String, Object> calculateStartDate(String mrpEventTypeId, int
facilityDaysToShip, int supplierLeadTimeDays,
+ GenericValue routing, Delegator delegator, LocalDispatcher
dispatcher, GenericValue userLogin) {
Map<String, Object> result = null;
Timestamp endDate = (Timestamp) requiredByDate.clone();
Timestamp startDate = endDate;
- long timeToShip = daysToShip * 8 * 60 * 60 * 1000L;
+ long facilityTimeToShip = facilityDaysToShip * 8 * 60 * 60 * 1000L;
if (isBuilt) {
List<GenericValue> listRoutingTaskAssoc = null;
if (routing == null) {
@@ -188,7 +193,7 @@ public class ProposedOrder {
long totalTime =
ProductionRun.getEstimatedTaskTime(routingTask, quantity, dispatcher);
if (i == listRoutingTaskAssoc.size()) {
// add the daysToShip at the end of the routing
- totalTime += timeToShip;
+ totalTime += facilityTimeToShip;
}
startDate =
TechDataServices.addBackward(TechDataServices.getTechDataCalendar(routingTask),
endDate, totalTime);
// record the routingTask with the startDate associated
@@ -201,26 +206,59 @@ public class ProposedOrder {
Debug.logError("No routing found for product = " +
product.getString("productId"), MODULE);
}
} else {
- // the product is purchased
- // TODO: REVIEW this code
+ // SALES_ORDER_SHIP demand can lead MRP to propose purchased
replenishment.
+ // In that case, apply the seller facility's final shipping leg
before the upstream supplier lead time.
+ long supplierLeadTime = supplierLeadTimeDays * 8 * 60 * 60 * 1000L;
try {
- GenericValue techDataCalendar =
EntityQuery.use(product.getDelegator())
+ GenericValue techDataCalendar = EntityQuery.use(delegator)
.from("TechDataCalendar")
.where("calendarId", "SUPPLIER")
.queryOne();
- if (techDataCalendar != null) {
- startDate = TechDataServices.addBackward(techDataCalendar,
endDate, timeToShip);
- } else {
- startDate = UtilDateTime.addDaysToTimestamp(endDate,
-daysToShip);
+ if ("SALES_ORDER_SHIP".equals(mrpEventTypeId) &&
facilityDaysToShip > 0) {
+ GenericValue facilityCalendar =
getFacilityShippingCalendar(endDate, delegator);
+ if (facilityCalendar != null) {
+ startDate =
TechDataServices.addBackward(facilityCalendar, startDate, facilityTimeToShip);
+ } else {
+ startDate = UtilDateTime.addDaysToTimestamp(startDate,
-facilityDaysToShip);
+ }
+ }
+ if ("SALES_ORDER_SHIP".equals(mrpEventTypeId) &&
supplierLeadTimeDays > 0) {
+ if (techDataCalendar != null) {
+ startDate =
TechDataServices.addBackward(techDataCalendar, startDate, supplierLeadTime);
+ } else {
+ startDate = UtilDateTime.addDaysToTimestamp(startDate,
-supplierLeadTimeDays);
+ }
}
} catch (GenericEntityException e) {
- Debug.logError(e, "Error : reading SUPPLIER TechDataCalendar:
" + e.getMessage(), MODULE);
+ Debug.logError(e, "Error : reading purchased scheduling
calendars: " + e.getMessage(), MODULE);
}
}
requirementStartDate = startDate;
return result;
}
+ private GenericValue getFacilityShippingCalendar(Timestamp date, Delegator
delegator) throws GenericEntityException {
+ GenericValue facilityCalendar = EntityQuery.use(delegator)
+ .from("FacilityCalendar")
+ .where("facilityId", facilityId, "facilityCalendarTypeId",
"SHIPPING")
+ .filterByDate(date)
+ .orderBy("-fromDate")
+ .queryFirst();
+ if (facilityCalendar != null) {
+ GenericValue techDataCalendar = EntityQuery.use(delegator)
+ .from("TechDataCalendar")
+ .where("calendarId",
facilityCalendar.getString("calendarId"))
+ .queryOne();
+ if (techDataCalendar != null) {
+ return techDataCalendar;
+ }
+ }
+ return EntityQuery.use(delegator)
+ .from("TechDataCalendar")
+ .where("calendarId", "DEFAULT")
+ .queryOne();
+ }
+
/**
* calculate the ProposedOrder quantity and update the quantity property.