This is an automated email from the ASF dual-hosted git repository.
mridulpathak pushed a commit to branch release24.09
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/release24.09 by this push:
new 50fda6a189 Fixed: Reject shipments mixing ship groups with different
destinations (OFBIZ-5346) (backport to release24.09) (#1509)
50fda6a189 is described below
commit 50fda6a1893c4ccc4e1b75f4c9eedd8fbb44e6dd
Author: Mridul Pathak <[email protected]>
AuthorDate: Fri Jul 31 12:27:16 2026 +0530
Fixed: Reject shipments mixing ship groups with different destinations
(OFBIZ-5346) (backport to release24.09) (#1509)
Fixed: Reject shipments mixing ship groups with different destinations
(OFBIZ-5346)
Backport of #1508 to release24.09.
- A `Shipment` could receive items from `OrderItemShipGroup`s with
different
destinations, causing `InvoiceServices.createInvoicesFromShipments` to
tax
shipping charges against the wrong state (it uses only the primary ship
group's address).
- `issueOrderItemShipGrpInvResToShipment` now rejects issuing an item
whose
ship group resolves to a different destination (country/state/postal
code)
than the shipment's primary ship group, comparing the full (orderId,
shipGroupSeqId) pair to also catch cross-order collisions. Two ship
groups
that happen to use different address records for the same physical
address are not treated as a mismatch.
- `PackingSession.checkShipGroupDestinations()` adds the same check
earlier
in the manual Pack Order flow, before any `Shipment`/`ItemIssuance` rows
are created.
- Fixes a 2020 regression (`7fcde49d82` / OFBIZ-11462) that mixed ship
groups' items in `quickShipEntireOrder` when they shared a facility,
plus
a related early-return that could abort shipment creation for remaining
ship groups.
- Adds `ShipmentTests.groovy` coverage for all of the above.
Thanks to Christian Carlow for reporting this issue.
---
applications/product/config/ProductUiLabels.xml | 3 +
.../shipment/issuance/IssuanceServices.xml | 64 ++++-
.../ofbiz/product/shipment/ShipmentServices.groovy | 15 +-
.../ofbiz/shipment/packing/PackingSession.java | 54 ++++
.../product/product/test/ShipmentTests.groovy | 312 +++++++++++++++++++++
5 files changed, 439 insertions(+), 9 deletions(-)
diff --git a/applications/product/config/ProductUiLabels.xml
b/applications/product/config/ProductUiLabels.xml
index 145dbfae01..781d9f3f4e 100644
--- a/applications/product/config/ProductUiLabels.xml
+++ b/applications/product/config/ProductUiLabels.xml
@@ -23061,6 +23061,9 @@
<value xml:lang="zh">没有发放订单明细送货组库存预留到送货
${parameters.shipmentId},因为要发放的数量 ${parameters.quantity} 大于剩余的退货的数量
${orderItemShipGrpInvRes.quantity},订单 ${orderItemShipGrpInvRes.orderId}、订单明细
${orderItemShipGrpInvRes.orderItemSeqId}、库存明细
${orderItemShipGrpInvRes.inventoryItemId}</value>
<value xml:lang="zh-TW">沒有發放訂單細項出貨群組庫存預留到出貨
${parameters.shipmentId},因為要發放的數量 ${parameters.quantity} 大於剩餘的退貨的數量
${orderItemShipGrpInvRes.quantity},訂單 ${orderItemShipGrpInvRes.orderId}、訂單細項
${orderItemShipGrpInvRes.orderItemSeqId}、庫存細項
${orderItemShipGrpInvRes.inventoryItemId}</value>
</property>
+ <property key="ProductNotIssueOrderToShipmentShipGroupDestinationMismatch">
+ <value xml:lang="en">Not issuing Order Item Ship Group Inventory
Reservation to shipment ${parameters.shipmentId} because ship group
${orderItemShipGrpInvRes.shipGroupSeqId} for order
${orderItemShipGrpInvRes.orderId} has a different destination than ship group
${shipment.primaryShipGroupSeqId} already associated with this shipment</value>
+ </property>
<property key="ProductNotIssueToFixedAssetMaintQuantityLess">
<value xml:lang="de">Füge Bestandsposition nicht zu Anlagegütern
${fixedAssetMaint.fixedAssetId:fixedAssetMaint.maintHistSeqId} hinzu da die
Menge ${parameters.quantity} kleiner oder gleich 0 ist für Bestandsposition
${inventoryItem.inventoryItemId}</value>
<value xml:lang="en">Not issuing InventoryItem to FixedAssetMaint
${fixedAssetMaint.fixedAssetId:fixedAssetMaint.maintHistSeqId} because the
quantity to issue ${parameters.quantity} is less than or equal to 0 for
inventoryItem ${inventoryItem.inventoryItemId}</value>
diff --git
a/applications/product/minilang/shipment/issuance/IssuanceServices.xml
b/applications/product/minilang/shipment/issuance/IssuanceServices.xml
index 53f2322e1d..80d4f806cd 100644
--- a/applications/product/minilang/shipment/issuance/IssuanceServices.xml
+++ b/applications/product/minilang/shipment/issuance/IssuanceServices.xml
@@ -129,14 +129,74 @@ under the License.
</add-error>
</if-compare-field>
+ <!-- get shipment -->
+ <entity-one entity-name="Shipment" value-field="shipment">
+ <field-map field-name="shipmentId"
from-field="parameters.shipmentId"/>
+ </entity-one>
+
+ <!-- reject if this ship group's destination is in a different taxing
jurisdiction than the
+ shipment's primary ship group; compare the full (orderId,
shipGroupSeqId) pair, since
+ shipGroupSeqId restarts at 00001 per order -->
+ <if-not-empty field="orderItemShipGrpInvRes">
+ <if-not-empty field="shipment.primaryShipGroupSeqId">
+ <if>
+ <condition>
+ <or>
+ <if-compare-field
field="orderItemShipGrpInvRes.orderId" operator="not-equals"
+ to-field="shipment.primaryOrderId"/>
+ <if-compare-field
field="orderItemShipGrpInvRes.shipGroupSeqId" operator="not-equals"
+ to-field="shipment.primaryShipGroupSeqId"/>
+ </or>
+ </condition>
+ <then>
+ <entity-one entity-name="OrderItemShipGroup"
value-field="primaryShipGroupDestCheck">
+ <field-map field-name="orderId"
from-field="shipment.primaryOrderId"/>
+ <field-map field-name="shipGroupSeqId"
from-field="shipment.primaryShipGroupSeqId"/>
+ </entity-one>
+ <get-related-one value-field="primaryShipGroupDestCheck"
relation-name="PostalAddress"
+ to-value-field="primaryShipGroupDestAddress"/>
+
+ <entity-one entity-name="OrderItemShipGroup"
value-field="incomingShipGroupDestCheck">
+ <field-map field-name="orderId"
from-field="orderItemShipGrpInvRes.orderId"/>
+ <field-map field-name="shipGroupSeqId"
from-field="orderItemShipGrpInvRes.shipGroupSeqId"/>
+ </entity-one>
+ <get-related-one value-field="incomingShipGroupDestCheck"
relation-name="PostalAddress"
+ to-value-field="incomingShipGroupDestAddress"/>
+
+ <!-- only reject when both sides resolve to a known
address in a different
+ country/state/postal code -->
+ <if-not-empty field="primaryShipGroupDestAddress">
+ <if-not-empty field="incomingShipGroupDestAddress">
+ <if>
+ <condition>
+ <or>
+ <if-compare-field
field="primaryShipGroupDestAddress.countryGeoId" operator="not-equals"
+
to-field="incomingShipGroupDestAddress.countryGeoId"/>
+ <if-compare-field
field="primaryShipGroupDestAddress.stateProvinceGeoId" operator="not-equals"
+
to-field="incomingShipGroupDestAddress.stateProvinceGeoId"/>
+ <if-compare-field
field="primaryShipGroupDestAddress.postalCode" operator="not-equals"
+
to-field="incomingShipGroupDestAddress.postalCode"/>
+ </or>
+ </condition>
+ <then>
+ <add-error>
+ <fail-property
resource="ProductUiLabels"
property="ProductNotIssueOrderToShipmentShipGroupDestinationMismatch"/>
+ </add-error>
+ </then>
+ </if>
+ </if-not-empty>
+ </if-not-empty>
+ </then>
+ </if>
+ </if-not-empty>
+ </if-not-empty>
+
<check-errors/>
<!-- get orderItem -->
<entity-one entity-name="OrderItem" value-field="orderItem"/>
<!-- get inventoryItem -->
<entity-one entity-name="InventoryItem" value-field="inventoryItem"/>
- <!-- get shipment -->
- <entity-one entity-name="Shipment" value-field="shipment"/>
<!-- get orderShipment -->
<make-value entity-name="OrderShipment"
value-field="orderShipmentLookupPk"/>
<set-pk-fields value-field="orderShipmentLookupPk" map="parameters"/>
diff --git
a/applications/product/src/main/groovy/org/apache/ofbiz/product/shipment/ShipmentServices.groovy
b/applications/product/src/main/groovy/org/apache/ofbiz/product/shipment/ShipmentServices.groovy
index e2c6d1cf8c..7d7ca12f8f 100644
---
a/applications/product/src/main/groovy/org/apache/ofbiz/product/shipment/ShipmentServices.groovy
+++
b/applications/product/src/main/groovy/org/apache/ofbiz/product/shipment/ShipmentServices.groovy
@@ -911,9 +911,9 @@ Map getOrderItemShipGroupLists(GenericValue orderHeader) {
List orderItemShipGroupList = orderHeader.getRelated('OrderItemShipGroup',
null, null, false)
// group orderItems (actually OrderItemAndShipGroupAssocs) by
shipGroupSeqId in a Map with List values
// This Map is actually used only for sales orders' shipments right now.
- List orderItemListByShGrpMap = []
+ Map orderItemListByShGrpMap = [:]
for (GenericValue orderItemAndShipGroupAssoc :
orderItemAndShipGroupAssocList) {
- orderItemListByShGrpMap << orderItemAndShipGroupAssoc
+
orderItemListByShGrpMap.computeIfAbsent(orderItemAndShipGroupAssoc.shipGroupSeqId)
{ [] } << orderItemAndShipGroupAssoc
}
Map result = success()
result.orderItemListByShGrpMap = orderItemListByShGrpMap
@@ -925,7 +925,7 @@ Map getOrderItemShipGroupLists(GenericValue orderHeader) {
/**
* Sub-method used by quickShip methods to create a shipment
*/
-Map createShipmentForFacilityAndShipGroup(GenericValue orderHeader, List
orderItemListByShGrpMap,
+Map createShipmentForFacilityAndShipGroup(GenericValue orderHeader, Map
orderItemListByShGrpMap,
List orderItemShipGroupList, List
orderItemAndShipGroupAssocList,
String
orderItemShipGrpInvResFacilityId,
Timestamp eventDate, Boolean
setPackedOnly) {
@@ -937,12 +937,13 @@ Map createShipmentForFacilityAndShipGroup(GenericValue
orderHeader, List orderIt
// for OrderItemShipGroup need to split all OISGIRs into their ship groups
and create a shipment for each
GenericValue facility = from('Facility').where(facilityId:
orderItemShipGrpInvResFacilityId).cache().queryOne()
for (GenericValue orderItemShipGroup : orderItemShipGroupList) {
- List perShipGroupItemList = orderItemListByShGrpMap
- // make sure we have something to ship
+ List perShipGroupItemList =
orderItemListByShGrpMap[orderItemShipGroup.shipGroupSeqId] ?: []
+ // skip to the next ship group instead of aborting the remaining ones
if (!perShipGroupItemList) {
List argListNames = [orderItemShipGroup.shipGroupSeqId]
- return success(UtilProperties.getMessage('ProductUiLabels',
- 'FacilityShipmentNoItemsAvailableToShip', argListNames,
locale))
+ successMessageList << UtilProperties.getMessage('ProductUiLabels',
+ 'FacilityShipmentNoItemsAvailableToShip', argListNames,
locale)
+ continue
}
// create the shipment for this facility and ship group combination
Map shipmentContext = [primaryOrderId: orderHeader.orderId,
diff --git
a/applications/product/src/main/java/org/apache/ofbiz/shipment/packing/PackingSession.java
b/applications/product/src/main/java/org/apache/ofbiz/shipment/packing/PackingSession.java
index 4b33107df7..9a610cbf1a 100644
---
a/applications/product/src/main/java/org/apache/ofbiz/shipment/packing/PackingSession.java
+++
b/applications/product/src/main/java/org/apache/ofbiz/shipment/packing/PackingSession.java
@@ -962,6 +962,8 @@ public class PackingSession implements java.io.Serializable
{
// check for errors
this.checkReservations(force);
+ // reject packing ship groups with different destination addresses
onto one shipment
+ this.checkShipGroupDestinations();
// set the status to 0
this.status = 0;
// create the shipment
@@ -1013,6 +1015,58 @@ public class PackingSession implements
java.io.Serializable {
}
}
+ /**
+ * Check that all packed lines' ship groups share the same destination
address.
+ * @throws GeneralException the general exception
+ */
+ protected void checkShipGroupDestinations() throws GeneralException {
+ Delegator delegator = this.getDelegator();
+ Map<String, String> jurisdictionByShipGroupKey = new HashMap<>();
+ for (PackingSessionLine line : this.getLines()) {
+ String shipGroupKey = line.getOrderId() + "/" +
line.getShipGroupSeqId();
+ if (jurisdictionByShipGroupKey.containsKey(shipGroupKey)) {
+ continue;
+ }
+ GenericValue destinationAddress;
+ try {
+ GenericValue orderItemShipGroup =
EntityQuery.use(delegator).from("OrderItemShipGroup")
+ .where("orderId", line.getOrderId(), "shipGroupSeqId",
line.getShipGroupSeqId())
+ .queryOne();
+ destinationAddress = orderItemShipGroup != null
+ ? orderItemShipGroup.getRelatedOne("PostalAddress",
false) : null;
+ } catch (GenericEntityException e) {
+ throw new GeneralException(e.getMessage());
+ }
+ jurisdictionByShipGroupKey.put(shipGroupKey,
destinationJurisdictionKey(destinationAddress));
+ }
+
+ Set<String> distinctJurisdictions = new
HashSet<>(jurisdictionByShipGroupKey.values());
+ distinctJurisdictions.remove(null);
+ if (distinctJurisdictions.size() > 1) {
+ throw new GeneralException("Cannot pack items from ship groups
with different destinations onto"
+ + " the same shipment: " +
jurisdictionByShipGroupKey.keySet() + " [104]");
+ }
+ }
+
+ /**
+ * Builds a key identifying the country/state/postal code of a destination
address, or {@code null}
+ * if the address (or all three of those fields) is unknown.
+ * @param destinationAddress the destination address
+ * @return the jurisdiction key
+ */
+ private static String destinationJurisdictionKey(GenericValue
destinationAddress) {
+ if (destinationAddress == null) {
+ return null;
+ }
+ String countryGeoId = destinationAddress.getString("countryGeoId");
+ String stateProvinceGeoId =
destinationAddress.getString("stateProvinceGeoId");
+ String postalCode = destinationAddress.getString("postalCode");
+ if (countryGeoId == null && stateProvinceGeoId == null && postalCode
== null) {
+ return null;
+ }
+ return countryGeoId + "|" + stateProvinceGeoId + "|" + postalCode;
+ }
+
/**
* Check empty lines.
* @throws GeneralException the general exception
diff --git
a/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/ShipmentTests.groovy
b/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/ShipmentTests.groovy
index 7bc600978e..7519fbcaaa 100644
---
a/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/ShipmentTests.groovy
+++
b/applications/product/src/test/groovy/org/apache/ofbiz/product/product/test/ShipmentTests.groovy
@@ -177,6 +177,186 @@ class ShipmentTests extends OFBizTestCase {
assert shipmentReceipt.productId == serviceCtx.productId
}
+ void testIssueOrderItemShipGrpInvResToShipmentRejectsMixedDestinations() {
+ Map orderResult = dispatcher.runSync('createTestSalesOrderSingle',
+ [userLogin: userLogin, productId: 'GZ-2644'])
+ assert ServiceUtil.isSuccess(orderResult)
+ String orderId = orderResult.orderId
+ assert orderId
+
+ GenericValue orderItem = from('OrderItem').where('orderId',
orderId).queryFirst()
+ assert orderItem
+ String orderItemSeqId = orderItem.orderItemSeqId
+
+ GenericValue shipGroup1 = from('OrderItemShipGroup')
+ .where('orderId', orderId, 'shipGroupSeqId', '00001')
+ .queryOne()
+ assert shipGroup1
+ assert shipGroup1.contactMechId
+
+ Map createShipmentCtx = [
+ shipmentTypeId: 'SALES_SHIPMENT',
+ statusId: 'SHIPMENT_INPUT',
+ primaryOrderId: orderId,
+ primaryShipGroupSeqId: '00001',
+ originFacilityId: 'WebStoreWarehouse',
+ userLogin: userLogin
+ ]
+ Map createShipmentResult = dispatcher.runSync('createShipment',
createShipmentCtx)
+ assert ServiceUtil.isSuccess(createShipmentResult)
+ String shipmentId = createShipmentResult.shipmentId
+ assert shipmentId
+
+ GenericValue shipment = from('Shipment').where('shipmentId',
shipmentId).queryOne()
+ assert shipment.destinationContactMechId == shipGroup1.contactMechId
+
+ String differentContactMechId =
resolveDifferentDestinationAddress('789 Different St')
+ assert differentContactMechId != shipGroup1.contactMechId
+ addShipGroup(orderId, '00002', differentContactMechId)
+ GenericValue inventoryItem = findGz2644InventoryItem()
+ associateItemWithShipGroup(orderId, orderItemSeqId, '00002', new
BigDecimal('1'))
+ reserveInventoryToShipGroup(orderId, orderItemSeqId, '00002',
inventoryItem.inventoryItemId, new BigDecimal('1'))
+
+ Map issueCtx = [
+ shipmentId: shipmentId,
+ orderId: orderId,
+ shipGroupSeqId: '00002',
+ orderItemSeqId: orderItemSeqId,
+ inventoryItemId: inventoryItem.inventoryItemId,
+ quantity: new BigDecimal('1'),
+ userLogin: userLogin
+ ]
+ Map issueResult =
dispatcher.runSync('issueOrderItemShipGrpInvResToShipment', issueCtx)
+ assert ServiceUtil.isError(issueResult)
+ assert ServiceUtil.getErrorMessage(issueResult).contains('different
destination')
+ }
+
+ void
testIssueOrderItemShipGrpInvResToShipmentRejectsMixedDestinationsAcrossOrders()
{
+ Map orderResult1 = dispatcher.runSync('createTestSalesOrderSingle',
+ [userLogin: userLogin, productId: 'GZ-2644'])
+ assert ServiceUtil.isSuccess(orderResult1)
+ String orderId1 = orderResult1.orderId
+ assert orderId1
+
+ GenericValue shipGroup1 = from('OrderItemShipGroup')
+ .where('orderId', orderId1, 'shipGroupSeqId', '00001')
+ .queryOne()
+ assert shipGroup1
+ assert shipGroup1.contactMechId
+
+ Map createShipmentCtx = [
+ shipmentTypeId: 'SALES_SHIPMENT',
+ statusId: 'SHIPMENT_INPUT',
+ primaryOrderId: orderId1,
+ primaryShipGroupSeqId: '00001',
+ originFacilityId: 'WebStoreWarehouse',
+ userLogin: userLogin
+ ]
+ Map createShipmentResult = dispatcher.runSync('createShipment',
createShipmentCtx)
+ assert ServiceUtil.isSuccess(createShipmentResult)
+ String shipmentId = createShipmentResult.shipmentId
+ assert shipmentId
+
+ // second, independent order; also uses shipGroupSeqId '00001' but a
different destination
+ Map orderResult2 = dispatcher.runSync('createTestSalesOrderSingle',
+ [userLogin: userLogin, productId: 'GZ-2644'])
+ assert ServiceUtil.isSuccess(orderResult2)
+ String orderId2 = orderResult2.orderId
+ assert orderId2
+ assert orderId2 != orderId1
+
+ GenericValue orderItem2 = from('OrderItem').where('orderId',
orderId2).queryFirst()
+ assert orderItem2
+ String orderItemSeqId2 = orderItem2.orderItemSeqId
+
+ String differentContactMechId =
resolveDifferentDestinationAddress('999 Cross Order Ave')
+ assert differentContactMechId != shipGroup1.contactMechId
+
+ Map updateShipGroupCtx = [
+ orderId: orderId2,
+ shipGroupSeqId: '00001',
+ contactMechId: differentContactMechId,
+ contactMechPurposeTypeId: 'SHIPPING_LOCATION',
+ userLogin: userLogin
+ ]
+ Map updateShipGroupResult =
dispatcher.runSync('updateOrderItemShipGroup', updateShipGroupCtx)
+ assert ServiceUtil.isSuccess(updateShipGroupResult)
+
+ // order2's item is already associated with ship group 00001 from
checkout; only reserve
+ GenericValue inventoryItem = findGz2644InventoryItem()
+ reserveInventoryToShipGroup(orderId2, orderItemSeqId2, '00001',
inventoryItem.inventoryItemId, new BigDecimal('1'))
+
+ Map issueCtx = [
+ shipmentId: shipmentId,
+ orderId: orderId2,
+ shipGroupSeqId: '00001',
+ orderItemSeqId: orderItemSeqId2,
+ inventoryItemId: inventoryItem.inventoryItemId,
+ quantity: new BigDecimal('1'),
+ userLogin: userLogin
+ ]
+ Map issueResult =
dispatcher.runSync('issueOrderItemShipGrpInvResToShipment', issueCtx)
+ assert ServiceUtil.isError(issueResult)
+ assert ServiceUtil.getErrorMessage(issueResult).contains('different
destination')
+ }
+
+ void testPackingSessionRejectsMixedShipGroupDestinations() {
+ Map orderResult = dispatcher.runSync('createTestSalesOrderSingle',
+ [userLogin: userLogin, productId: 'GZ-2644'])
+ assert ServiceUtil.isSuccess(orderResult)
+ String orderId = orderResult.orderId
+ assert orderId
+
+ GenericValue orderItem = from('OrderItem').where('orderId',
orderId).queryFirst()
+ assert orderItem
+ String orderItemSeqId = orderItem.orderItemSeqId
+
+ String differentContactMechId =
resolveDifferentDestinationAddress('456 Other Ave')
+ addShipGroup(orderId, '00002', differentContactMechId)
+ GenericValue inventoryItem = findGz2644InventoryItem()
+ associateItemWithShipGroup(orderId, orderItemSeqId, '00002', new
BigDecimal('1'))
+ reserveInventoryToShipGroup(orderId, orderItemSeqId, '00002',
inventoryItem.inventoryItemId, new BigDecimal('1'))
+
+ PackingSession packingSession = new PackingSession(dispatcher,
userLogin)
+
+ Map packLine1Ctx = [
+ productId: 'GZ-2644',
+ orderId: orderId,
+ shipGroupSeqId: '00001',
+ quantity: new BigDecimal('1'),
+ packageSeq: 1,
+ pickerPartyId: 'DemoCustomer',
+ packingSession: packingSession,
+ userLogin: userLogin
+ ]
+ Map packLine1Result = dispatcher.runSync('packSingleItem',
packLine1Ctx)
+ assert ServiceUtil.isSuccess(packLine1Result)
+
+ Map packLine2Ctx = [
+ productId: 'GZ-2644',
+ orderId: orderId,
+ shipGroupSeqId: '00002',
+ quantity: new BigDecimal('1'),
+ packageSeq: 1,
+ pickerPartyId: 'DemoCustomer',
+ packingSession: packingSession,
+ userLogin: userLogin
+ ]
+ Map packLine2Result = dispatcher.runSync('packSingleItem',
packLine2Ctx)
+ assert ServiceUtil.isSuccess(packLine2Result)
+
+ Map completeCtx = [
+ orderId: orderId,
+ pickerPartyId: 'DemoCustomer',
+ packingSession: packingSession,
+ forceComplete: true,
+ userLogin: userLogin
+ ]
+ Map completeResult = dispatcher.runSync('completePack', completeCtx)
+ assert ServiceUtil.isError(completeResult)
+ assert ServiceUtil.getErrorMessage(completeResult).contains('[104]')
+ }
+
void testCreateShipmentRouteSegment() {
GenericValue shipment = from('Shipment')
.where('shipmentId', '9998')
@@ -201,4 +381,136 @@ class ShipmentTests extends OFBizTestCase {
assert shipmentRouteSegment.shipmentRouteSegmentId ==
shipmentRouteSegmentId
}
+ void testQuickShipEntireOrderDoesNotMixShipGroupDestinations() {
+ Map orderResult = dispatcher.runSync('createTestSalesOrderSingle',
+ [userLogin: userLogin, productId: 'GZ-2644'])
+ assert ServiceUtil.isSuccess(orderResult)
+ String orderId = orderResult.orderId
+ assert orderId
+
+ GenericValue orderItem = from('OrderItem').where('orderId',
orderId).queryFirst()
+ assert orderItem
+ String orderItemSeqId = orderItem.orderItemSeqId
+
+ String differentContactMechId =
resolveDifferentDestinationAddress('321 Another Rd')
+ addShipGroup(orderId, '00002', differentContactMechId)
+ GenericValue inventoryItem = findGz2644InventoryItem()
+ associateItemWithShipGroup(orderId, orderItemSeqId, '00002', new
BigDecimal('1'))
+ reserveInventoryToShipGroup(orderId, orderItemSeqId, '00002',
inventoryItem.inventoryItemId, new BigDecimal('1'))
+
+ Map quickShipResult = dispatcher.runSync('quickShipEntireOrder',
[orderId: orderId, userLogin: userLogin])
+ assert ServiceUtil.isSuccess(quickShipResult)
+
+ List<GenericValue> shipments =
from('Shipment').where('primaryOrderId', orderId).queryList()
+ assert shipments.size() == 2
+
+ for (GenericValue shipment : shipments) {
+ List<GenericValue> itemIssuances =
from('ItemIssuance').where('shipmentId', shipment.shipmentId).queryList()
+ Set<String> shipGroupsOnThisShipment =
itemIssuances*.shipGroupSeqId as Set
+ assert shipGroupsOnThisShipment.size() == 1
+ assert
shipGroupsOnThisShipment.contains(shipment.primaryShipGroupSeqId)
+ }
+ }
+
+ void testQuickShipEntireOrderSkipsShipGroupWithNothingToShip() {
+ Map orderResult = dispatcher.runSync('createTestSalesOrderSingle',
+ [userLogin: userLogin, productId: 'GZ-2644'])
+ assert ServiceUtil.isSuccess(orderResult)
+ String orderId = orderResult.orderId
+ assert orderId
+
+ GenericValue orderItem = from('OrderItem').where('orderId',
orderId).queryFirst()
+ assert orderItem
+
+ // Ship group 00002: created but given NO order items and NO inventory
reservation —
+ // this is the "nothing to ship for this group" case.
+ String secondContactMechId = resolveDifferentDestinationAddress('555
Empty Group Rd')
+ addShipGroup(orderId, '00002', secondContactMechId)
+
+ // orderItemShipGroupList (from getRelated('OrderItemShipGroup')) has
no guaranteed order,
+ // so this test only proves correctness if ship group 00001 (which HAS
items) still gets
+ // shipped regardless of iteration order relative to the empty 00002.
+ Map quickShipResult = dispatcher.runSync('quickShipEntireOrder',
[orderId: orderId, userLogin: userLogin])
+ assert ServiceUtil.isSuccess(quickShipResult)
+
+ List<GenericValue> shipments =
from('Shipment').where('primaryOrderId', orderId).queryList()
+ assert shipments.size() == 1
+ assert shipments[0].primaryShipGroupSeqId == '00001'
+
+ List<GenericValue> itemIssuances =
from('ItemIssuance').where('shipmentId', shipments[0].shipmentId).queryList()
+ assert itemIssuances
+ assert itemIssuances*.shipGroupSeqId as Set == ['00001'] as Set
+ }
+
+ /**
+ * Resolves a postal address in a different country/state/postal code than
the demo customer's
+ * default shipping address, returning its contactMechId.
+ */
+ private String resolveDifferentDestinationAddress(String addressStreet) {
+ Map createAddressCtx = [
+ address1: addressStreet,
+ city: 'New York',
+ stateProvinceGeoId: 'NY',
+ postalCode: '10001',
+ countryGeoId: 'USA',
+ userLogin: userLogin
+ ]
+ Map createAddressResult = dispatcher.runSync('createPostalAddress',
createAddressCtx)
+ assert ServiceUtil.isSuccess(createAddressResult)
+ String contactMechId = createAddressResult.contactMechId
+ assert contactMechId
+ return contactMechId
+ }
+
+ /** Adds a splittable OrderItemShipGroup with the given destination. */
+ private void addShipGroup(String orderId, String shipGroupSeqId, String
contactMechId) {
+ Map createShipGroupCtx = [
+ orderId: orderId,
+ shipGroupSeqId: shipGroupSeqId,
+ contactMechId: contactMechId,
+ facilityId: 'WebStoreWarehouse',
+ maySplit: 'Y',
+ userLogin: userLogin
+ ]
+ Map createShipGroupResult =
dispatcher.runSync('createOrderItemShipGroup', createShipGroupCtx)
+ assert ServiceUtil.isSuccess(createShipGroupResult)
+ }
+
+ /** Associates an order item with a ship group for the given quantity. */
+ private void associateItemWithShipGroup(String orderId, String
orderItemSeqId, String shipGroupSeqId, BigDecimal quantity) {
+ Map createAssocCtx = [
+ orderId: orderId,
+ orderItemSeqId: orderItemSeqId,
+ shipGroupSeqId: shipGroupSeqId,
+ quantity: quantity,
+ userLogin: userLogin
+ ]
+ Map createAssocResult =
dispatcher.runSync('addOrderItemShipGroupAssoc', createAssocCtx)
+ assert ServiceUtil.isSuccess(createAssocResult)
+ }
+
+ /** Reserves inventory for an order item under a ship group. */
+ private void reserveInventoryToShipGroup(String orderId, String
orderItemSeqId, String shipGroupSeqId,
+ String inventoryItemId, BigDecimal quantity) {
+ Map createInvResCtx = [
+ orderId: orderId,
+ shipGroupSeqId: shipGroupSeqId,
+ orderItemSeqId: orderItemSeqId,
+ inventoryItemId: inventoryItemId,
+ quantity: quantity,
+ userLogin: userLogin
+ ]
+ Map createInvResResult =
dispatcher.runSync('createOrderItemShipGrpInvRes', createInvResCtx)
+ assert ServiceUtil.isSuccess(createInvResResult)
+ }
+
+ /** Finds an available GZ-2644 inventory item at WebStoreWarehouse. */
+ private GenericValue findGz2644InventoryItem() {
+ GenericValue inventoryItem = from('InventoryItem')
+ .where('productId', 'GZ-2644', 'facilityId',
'WebStoreWarehouse')
+ .queryFirst()
+ assert inventoryItem
+ return inventoryItem
+ }
+
}