Preprocess Packing Solution Scripts

Updated 3 months ago by Colin Mollenhour

Before ShipStream generates a Packing Solution for a given set of items it will run these items through any "Preprocess Packing Solution" type scripts that the user has defined. These scripts provide the following capabilities:

  • Force certain items to be grouped together
  • Distribute items to separate packages using a specific group size
  • Force a set of items to use a specific container or set of containers
  • Set a specific shipping method for a set of items
  • Set a specific Picking Class for a set of items

Scripts are an advanced feature meant to handle complex or abnormal scenarios. There are many things you can do with scripts but it may not always be necessary to use scripts. See Product Packaging Options and Packing Solutions for more information.

The bubble highlighted in red indicates where on the Order Processing Loop the Preprocess script evaluation will occur:

Creating a Script

To create a script go to System > Scripts and click "Create New Script". Choose the merchants it should apply to, provide a brief description of what the script will do and choose "Preprocess Packing Solution" as the Type. When you are ready for the script to be active set "Is Active" to "Yes".

Script Authoring

Scripts in ShipStream are written in a programming language commonly called Javascript and evaluated using the V8 interpreter which understands ECMAScript 2015 (ES6). For more information see Scripting Basics.

The scripts with type "Preprocess Packing Solution" are given two variables in the execution scope, request and items.

Effectively, the environment is similar to the following script:

const request = {
"merchant": "acme",
"store": "acmebrand",
"warehouse_id": 1,
"shipping_method": "ups_03",
"saturday_delivery": false,
"signature_required": "none",
"overbox": false,
"destination": {
"company": "",
"street": "",
"city": "",
"region": "",
"postcode": "",
"country": "US",
"classification": "res"
}
}
let items = [
{
"product_id": 333,
"qty": 1,
"unit_qty": 1,
"product": {
"sku": "ABC",
"name": "ABC Widget",
"<product_attribute_code>": "<value>"
}
}
]

// ShipStream embedded environment emulator
class Bucket {
constructor() {}
addItem (item, itemQty, unitQty) {}
setGroupSize (size) {}
setContainer (sku) {}
setLength(value) {}
setWidth(value) {}
setHeight(value) {}
setContainers (skus) {}
setInfill (sku) {}
setShippingMethod (method) {}
setPickingClass (pickingClass) {}
setProviders (providers) {}
addComment (message) {}
}
const ShipStream = {
createBucket: function () {
return new Bucket()
}
}

(function (request, items, ShipStream) {
// Your code goes here
})(request, items, ShipStream)

There can be multiple scripts eligible for the same scope, in which case each script will be run in succession by the sort order ascending. Adding items to a bucket takes them out of scope for the next script until there are no more unassigned items at which point no further scripts will be evaluated. Empty buckets will be ignored and unassigned items will be treated as if they were added to a separate bucket with no special properties.

ShipStream.createBucket()

The only way that Preprocess scripts can affect the outcome of the Packing Solution process is to create one or more "buckets" and add items to the bucket. Each bucket will effectively become one or more shipments at the end of execution. Any items not in a bucket will be passed on to the "Resolve Packing Solutions for all providers" stage of the Order Processing Loop. Additionally, any buckets that have multiple container SKUs or true specified using setContainers will also be passed on separately to the "Resolve Packing Solutions for all providers" stage for separate evaluation as a set of order items.

let myBucket = ShipStream.createBucket()

The following methods are available on each bucket object. The methods all return the bucket object so they may be chained together.

addItem(item, itemQty?, unitQty?) {}

Buckets only affect the items that are added to them using this method. The parameters are:

  • item - A member of the items array that is in the scope of the script.
  • itemQty - If specified, this many items will be added to the bucket, otherwise the entire amount available will be added to the bucket.
  • unitQty - If specified, the itemQty will be multiplied by this amount when applying the group size (see setGroupSize).

Here is a simple example of adding one unit of the first item to a separate bucket.

let myBucket = ShipStream.createBucket()
myBucket.addItem(items[0], 1)

setGroupSize(size) {}

For items in this bucket, create a separate package for every size number of units.

If unitQty was specified for addItem, the group size will be relative to the unitQty instead of the itemQty.

Example

Given the following partial script:

myBucket.addItem(itemA, itemA.qty, 1)
myBucket.addItem(itemB, itemB.qty, 6)
myBucket.addItem(itemC, itemC.qty, 12)
myBucket.setGroupSize(24)

The items added to myBucket will result in packages that have one of the following combinations depending on the quantity.

Item Object

Quantity

itemA

24

itemA

itemB

12

2

itemA

itemC

12

1

itemA

itemB

itemC

6

1

1

itemB

4

itemB

itemC

2

1

itemC

2

setContainer(sku) {}

Specify a sku of a container that should be used for this bucket. This will cause all Packing Solution algorithms to be skipped and it will be assumed that the items added to the bucket will fit in the container and that no other, more optimal solution exists.

Example

myBucket.setContainer('ExtraHeavyDutyBox')

setLength(value), setWidth(value), setHeight(value)

If you specify a container using setContainer(sku) as per above, the container's dimensions will be used as the final package dimensions. However, if the container does not have dimensions (e.g. if it is a "non-overbox" container) or you need to for some reason override those dimensions you can set them with these methods.

myBucket.setLength(item.product.length)
myBucket.setWidth(item.product.width * 2)
myBucket.setHeight(item.product.height)

setContainers(skus) {}

Specify a list of skus of containers that should be used when applying the Packing Solution algorithms. This overrides "Use for Bin Packing: No" so could either be a subset of the normal containers list or some specific containers that aren't usually considered when evaluating Packing Solutions.

Pass true to use the default set of containers available for bin packing.

Example

myBucket.setContainers(['ExtraHeavyDutyBox_1','ExtraHeavyDutyBox_2'])
myBucket.setContainers(true)

setInfill(sku) {}

Specify a sku of an Infill type Packaging Feature to force that infill to be used for the resulting shipment(s).

The default value null will determine the infill automatically based on the configuration.

Specify false to disable infill for the bucket.

Example

myBucket.setInfill('Vermiculite_Fine')

setShippingMethod(method) {}

Specify a shipping method code to force a shipping method to be used for a subset of items.

Example

myBucket.setShippingMethod('fedex_SMART_POST')

setPickingClass(pickingClass) {}

Specify a Picking Class by name to force the resulting shipment(s) to be assigned to this Picking Class.

Example

myBucket.setPickingClass('Heavy/Bulky')

setProviders(providers) {}

Specify an array of provider codes to be used for resolving the packing solution. Valid provider options are "box_packer" and "paccurate". Passing an empty array will cause only the fallback weight-based method to be used and no container will be assigned.

Example

myBucket.setProviders([])

addComment(message) {}

The message will be logged to the Order history, helpful for keeping a historical record of what the script did to affect an order.

myBucket.addComment('Use special box and infill for bottled liquids.')

Cookbook

Following are some examples to show how all of these can be put together.

Load up our Codepen and click Fork to experiment with your own scripts!

Default behavior

Just for reference, the default behavior when there is no script is effectively equivalent to this:

let bucket = ShipStream.createBucket()
bucket.setContainers(true)
items.forEach(item => bucket.addItem(item))
Force all items into a single shipment for LTL orders
if (request.shipping_method.match(/LTL/)) {
let bucket = ShipStream.createBucket()
items.forEach(item => bucket.addItem(item))
}
Separate special items from other items

Identifies items with the "foo" attribute equal to "bar". If there are more than two of these SKUs they will be added to a separate bucket with a group size of 12 and the items with SKU ending in '-6pk' are given a unit quantity of 6.

let foobar = items.filter(item => item.product.foo == 'bar')
if
(foobar.length > 2) {

let bucket = ShipStream.createBucket()
bucket.setGroupSize(12)
foobar.forEach(item => bucket.addItem(
item
,
item
.qty,
item
.product.sku.match(/-6pk$/) ? item.qty * 6: item.qty)
)

}
Ship heavy items separately if there are more than 4 of them

Identifies items over 50 pounds and if there are 4 or more of such items they will all be shipped using the Heavy/Bulky Picking Class.

let heavy = items.filter(item => item.product.weight > 50)
let heavyQty = heavy.reduce((total, item) => total + item.qty, 0)
if (heavyQty >= 4) {
let bucket = ShipStream.createBucket()
bucket.setGroupSize(1).setPickingClass('Heavy/Bulky')
heavy.forEach(item => bucket.addItem(item))
}
Peanut butter and jelly

When peanut butter and jelly are ordered together we want to always ship them in pairs with one pair per package.

let pb = items.filter(item => item.product.sku.match('/^PB'))
let jelly = items.filter(item => item.product.sku.match('/^Jelly'))
if (pb.length && jelly.length) {
let jellyItem = jelly.shift()
while (pb.length) {
let pbItem = pb.shift()
for (let i = 0; i < pbItem.qty; i++) {
while (jellyItem.qty < 1 && jelly.length) {
jellyItem = jelly.shift()
}
if (jellyItem.qty > 0) {
ShipStream.createBucket()
.addItem(pbItem, 1)
.addItem(jellyItem, 1)
.setContainer('PB&J_box')
.setShippingMethod('usps_FC')
.addComment('PB&J always ship together via USPS First Class')
}
}
}
}


How did we do?