Skip to main content

Orchestration

Order orchestration is a Chapter of the Orders Module that gives you full creative freedom in constructing logic which EVA can follow in the order fulfillment process for delivery orders. This logic can be constructed according to your preferred delivery methods for certain orders. Using this logic, EVA can for example prioritize Ship-from-Store over warehouse fulfillment for certain orders.

Order orchestration can hold an array of so-called 'sheets'. Orders always go through all sheets. Based on the logic in a sheet, the order gets valued at a certain score for that particular sheet. The highest scoring sheet is ultimately used for fulfillment of the order. Unless you enable partial fulfillment.


Authorization

In order to be able to access this chapter, you need the SymphonySheets permission.

Sheet structure​

This sheet contains the logic, divided into different segments (scopes). There are currently eight possible scopes:

  1. Order
  2. Supplier
  3. OrderLine
  4. OrderLineSuppliers
  5. FulfillmentOptions
  6. Shipment
  7. Fanout
  8. FulfillmentProposition

Each scope contains their own logic, and each scope is able to utilize different properties.

Order​

The Order scope determines which orders are eligible for the fulfillment method.

Supplier​

The Supplier scope determines which organization units are eligible for the fulfillment method.

OrderLine​

The OrderLine scope determines which products (order lines) are eligible for the fulfillment method.

Price and amount property examples

score add OrderLine.UnitPrice

  • UnitPrice: the price (ex-tax) for a single unit of the item.
  • UnitPriceInTax: the price (in-tax) for a single unit of the item.
  • TotalAmount: the total amount for the OrderLine (ex-tax), before discounts.
  • TotalAmountInTax: the total amount for the OrderLine (in-tax), before discounts.
  • NetTotalAmount: the effective total amount for the OrderLine (in-tax), after discounts.
  • NetTotalAmountInTax: the effective total amount for the OrderLine (in-tax), after discounts.
  • DiscountAmount: the amount of discount given to the OrderLine. If there is no discount, this will be 0 and otherwise this will be a negative number.

You can also use this to scope based on Custom Product properties.

Custom product property example
require [Product.Content.SomeProperty] = 'value' 


PreferredFulfillmentOrganizationUnit​

The OrderLine object in the OrderLine scope lets you set a property called PreferredFulfillmentOrganizationUnit. This is an OU object that has properties available such as ID, Type and BackendID and makes it possible to set an OU to fulfill specific order lines.

You can use this property in your sheet to either 1) boost the score of the preferred OU or 2) add a Require that the supplier MUST be the preferred OU. Basically a soft vs hard filter.

Examples preferred fulfillment OU
Soft filter
scope OrderLine

score add 1000 when Supplier.ID = OrderLine.PreferredFulfillmentOrganizationUnit.ID

Hard filter
scope OrderLine

require Supplier.ID = OrderLine.PreferredFulfillmentOrganizationUnit.ID
when OrderLine.PreferredFulfillmentOrganizationUnit has value


OrderFulfillmentLine states​

Depending on AllowPartialFulfilment, the order is sent as either a single OrderLineFulfillmentLine (containg all OrderLines) or multiple ones to the applicable stores. This OrderFulfilmentLine can consequently have five different states, which depend on the corresponding SFS task in the store:

  • New: the corresponding task is sent out to the shop(s)
  • InProgress: the task has been started somewhere
  • Cancelled: the task has either been actively cancelled, or the task was cancelled automatically due to not adhering to requirements (e.g. not accepting in designated time, not completing an accepted task in time)
  • Deactivated: the task was deactivated at all shops that had the task available, but another store picked it up first
  • Completed: job's done

Recalculation

Whether an OrderLineFulfillmentLine has a Cancelled or Deactivated state is a consequence of its recalculation. That means that when order lines are run through orchestration for a second (or third) time, for any OrderFulfillmentLine that got the Cancelled state in a particular store, that same store will not qualify in orchestration to get assigned the order lines (contained in the cancelled OrderFulfillmentLine) again.

If the OrderFulfillmentLine was deactivated however, the order line(s) contained in that OrderFulfillmentLine will be offered again to the store, up to a set amount of times see MaxRetryCountForDeactivatedLines.

Cancellation reasons​

The SFS tasks can have various reasons for cancellation/deactivation. These reasons are returned in the Order history tab of an order in the following manner:

  • NULL - meaning no cancellation reason available / applicable
  • Otherstore - deactivated during fan out because another store has picked up the task already
  • FulfillmentCancelled - the orderline itself for which the task was open is cancelled
  • ​Unknown - we don’t know why the task was cancelled, which almost always entails that the cancellation was due to user intervention, e.g. cancelling the task in the Companion app or in Admin suite (but could technically also happen via API of course)
  • ​Expired - deadline ran out so we cancelled the task

OrderLineSuppliers​

The OrderLineSuppliers scope can be used to construct logic involving the amount of suppliers using the SupplierCount variable. For example, if more than 1 supplier is known, set the DeadLineInMinutes as (when SupplierCount = 1 then 1000 else then 10).

FulfillmentOptions​

The FulfillmentOptions scope allows you to reject fulfillment options for a supplier. It works by finding all possible suppliers for your order, followed by finding all possible fulfillment options for those suppliers. This may however include options we don't want to use for certain suppliers, such as same-day delivery for stores that don't want to do SDD.

To facilitate this, the fulfillment option and supplier combination is run past the new scope.

An example
scope FulfillmentOptions

require FulfillmentOption.SameDayDelivery.IsAvailable = false
when Supplier.BackendID in 'SHOP_1', 'SHOP_2' // this ensures SDD is not available for Shop 1 and 2

Shipment​

The Shipment scope allows you to reject or alter the score of a shipment proposal. You might for example not want an employee to go out with over 200 euro of products in a delivery. In that case you might state require TotalAmountInTax < 200 to prevent shipments over that amount being created.

Fanout​

The Fanout scope determines the maximum amount of organization units the order can be pushed towards in case the logic comes up with multiple eligible organization units.

FulfillmentProposition​

The FulfillmentProposition checks the potential outcome of orchestration sheets and can for example be used to prefer the option with the least amount of shipments.

danger

No scopes are mandatory. However, the order in which you use them is.

In addition to parameters, these scopes can contain certain logic that increase or decrease the sheet's score. This scoring system can then be utilized to ensure the desired outcome for certain orders.

Quantity splitting​

Some extra attention is warranted for quantity splitting. If the setting Orders:Default:AllowPartialFulfillment (as named earlier) is enabled, then quantity splitting will now be applicable as well.

This term indicates how each orderline with a quantity higher than 1 is essentially treated by the order orchestration as if it were multiple orderlines all containing a single item. This makes it possible to split the order across multiple suppliers.

In practice this means that even if there are no suppliers carrying the required number of products, the order can still be fulfilled by shipping the same product from multiple suppliers.

When the orderlines have split to be delivered from multiple suppliers, you can see this in the order's history in the Admin.

Recalculation​

The best possible path for some orders may lead to fulfillment by stores. Depending on the store's Ship from Store settings however, the store may be allowed to refuse picking certain orderlines. If not all orderlines are picked in their Shop from store task, then, naturally, those orderlines will not be packed and shipped either.

In that case the unpicked orderlines will return to the Order orchestration and the next best possible path for those specific products will be calculated while excluding the first store.

Orchestration interface in Suite​

When opening up the Order orchestration chapter, you are greeted by the sheets overview.


By default, the overview only exposes active sheets. In order to see inactive sheets, use the filter menu on the right side of the interface. This filter menu also allows you to filter sheets on Name or ID.

Preview​

Click the 'scroll' icon next to the + to open up your orchestration previewer. Here, you can specify an order number for any order you want, along with a date and time. EVA will then pull that order through your orchestration sheets and tell you what would've happened to that order if it was ordered at that specific time.

Below you can see an example of such a preview. At the top you can see the card where you enter the order ID, followed by a card containing the order orchestration's winning OU. The following cards contain OU's which could also be suitable for fulfillment and which stores were specifically excluded for fulfillment. By selecting a store from the dropdown in the rejections list, you can find the reasons for rejection. If applicable, you can also find sheets without attached supplier OU's which were rejected in this preview.


Checking orchestration results on the order​

Aside from checking the probable result of the order orchestration by means of the preview option, you can also check the results after the orchestration has taken place. These details of the orchestration are including:

  • Fulfillments that resulted from the orchestration (can be multiple);
  • The shipments each fulfillment contains;
  • Where each shipment gets its score from;
  • Which orderlines are in the shipment;
  • A list of order lines with each line having its own list of PotentialSuppliers;
  • Each supplier in ValidSuppliers having a list of order lines which the supplier could potentially fulfill;
  • Where each order line gets its score from.

In addition to the resulting fulfillments, the details also contain a list of all valid suppliers, both the one that won and all the ones that didn’t (meaning they met all requirements, but were beaten in score). And finally there is a list of all rejection reasons per supplier.

All this information is stored in a blob, which is attached to the OrderFulfillment itself. If there is fan out happening on the Order, there will be multiple OrderFulfillments (one for each supplier) but all of them will share the same blob with the same details.

Blob structure example

{
"Fulfillments": [
{
"Shipments": [
{
"FulfillmentAction": "SHIP_FROM_STORE",
"Sheet": {
"ID": 3,
"Name": "ShipFromStore"
},
"Supplier": {
"ID": 12,
"Name": "Urk store",
"BackendID": "urk_store"
},
"OrderLines": [
{
"ID": 59,
"Score": 100.0,
"Quantity": 1,
"ScoreExplanations": [
{
"Description": "100 added to score of OrderLine due to a boost in the Supplier scope.",
"Expression": "score add 100\n",
"PreviousScore": 0.0,
"NewScore": 100.0,
"DeltaScore": 100.0
}
]
},
{
"ID": 60,
"Score": 100.0,
"Quantity": 1,
"ScoreExplanations": [
{
"Description": "100 added to score of OrderLine due to a boost in the Supplier scope.",
"Expression": "score add 100\n",
"PreviousScore": 0.0,
"NewScore": 100.0,
"DeltaScore": 100.0
}
]
}
],
"Score": 200.0,
"Data": {
"TravelDistanceText": null,
"TravelTimeText": null,
"TravelTimeInMinutes": null,
"TravelDistanceInKm": null
},
"ScoreExplanation": {
"Description": "Shipment score consists of: 200 from score boosts on the Shipment scope and 200 from the OrderLines in this shipment"
}
}
],
"Score": 200.0,
"ScoreExplanation": {
"Description": "Fulfillment proposition score consists of: 0 from score boosts on the Fulfillment Proposition scope and 200 from the shipments in this proposition."
}
}
],
"OrderLines": [
{
"ID": 59,
"PotentialSuppliers": [
{
"OrganizationUnitID": 12,
"Score": 100.0,
"Sheet": "ShipFromStore"
},
{
"OrganizationUnitID": 11,
"Score": 0.0,
"Sheet": "Warehouse"
}
]
},
{
"ID": 60,
"PotentialSuppliers": [
{
"OrganizationUnitID": 12,
"Score": 100.0,
"Sheet": "ShipFromStore"
},
{
"OrganizationUnitID": 11,
"Score": 0.0,
"Sheet": "Warehouse"
}
]
}
],
"ValidSuppliers": [
{
"SupplierID": 11,
"SupplierName": "Lelystad warehouse",
"FulfillmentMethod": "Warehouse",
"Score": 0.0,
"ScoreExplanations": [],
"FulfillableOrderLineIDs": [
59,
60
]
},
{
"SupplierID": 12,
"SupplierName": "Urk store",
"FulfillmentMethod": "ShipFromStore",
"Score": 200.0,
"ScoreExplanations": [
{
"OrderLineID": 59,
"Score": 100.0,
"Description": "100 added to score of OrderLine due to a boost in the Supplier scope.",
"Expression": "score add 100\n"
},
{
"OrderLineID": 60,
"Score": 100.0,
"Description": "100 added to score of OrderLine due to a boost in the Supplier scope.",
"Expression": "score add 100\n"
}
],
"FulfillableOrderLineIDs": [
59,
60
]
}
],
"Rejections": [],
"HasErrors": false
}


Retrieving the results blob​

The details can -for now- only be retrieved by calling GetOrderFulfillment with the ID of the fulfillment (note: not an OrderID) and also specifying IncludeOrchestrationDetails as true.

Not retroactively and with 14-day lifespan

Note that this functionality does not work retroactively, only from the inception of this functionality with core drop 2.0.655. By default, these blobs will be stored for 14 days. This can however be changed by altering the Symphony:OrchestrationDetailsLifetimeInDays setting.

Sheet details​

When clicking a sheet, you open up the sheet editor. By default, you are directed to the visual editor.


The visual editor can be used to make simple edits in a more intuitive interface. However, the visual editor is ONLY meant for editing. New lines should always be added in the advanced editor.

Examples:

You want to add more stores to Ship-from-Store and your sheet already has an entry defining these stores β†’ you can use the visual editor to add a store by clicking the + icon and adding the store.

You want to exclude certain products from the sheet and you don't have an entry to exclude products yet β†’ you have to use the advanced editor.

You can switch to the advanced editor by clicking the chevron icon in the top-right corner.


The advanced editor is the main configuration page for order orchestration. This is where you construct the actual logic. The moon icon just above the right side of the code editor triggers dark mode for the editor.

The icon in the very top-right corner of the page switches you back to the visual editor.

Creating a new sheet​

Clicking the + button on the overview page opens up the page to create a new sheet.

Some ground rules It's safe to say that the logic within a sheet can be used to narrow down the amount of options for fulfillment. For efficiency purposes, logic that decreases the potential outcomes to the greatest extent, should be put first in a sheet.

Fulfillment​

Every sheet starts out with a fulfillment property. This property doesn't represent any functionality, it's just a unique identifier for your sheet. However, this property should only be set once, and never altered. Altering this property could cause some rerouting issues.

Example:

Python
fulfillment ShipFromStore

Action​

The second line after the sheet defines the sheet's action, which reflects the final action that's taken when the sheet has the highest score and will thus be used for fulfillment on the order(lines). As of now, there are only three actions; SHIP_FROM_STORE, EXPORT_ORDER and CANCEL. With CANCEL obviously cancelling the order. This is used as a fallback in case the logic doesn't come up with any possible fulfillment propositions.

Example:

Python
fulfillment ShipFromStore
action SHIP_FROM_STORE

Option​

There are four options available.

AllowPartialFulfillment​

AllowPartialFulfillment determines whether a specific sheet can be used for partial fulfillment or not. This setting enables orders to be partially fulfilled through Ship-from-Store and partially through the warehouse, or to split the order across multiple stores - with each OrderLine having its own OrderFulfillmentLine. If this option is set to false (default), orders can only be fulfilled using one sheet.


option AllowPartialFulfillment as true

DisableLineSplitting​

The second option depends on the first one, since it only applies if AllowPartialFulfillment is set to true. DisableLineSplitting is by default set to false, meaning the splitting of different products across stores/warehouses as well as splitting the quantities of products is allowed. By setting it to true per sheet however, you can fine tune what sheets are allowed to split the quantity of these lines and which aren't.


option DisableLineSplitting as true

Example scenario

Let's make this clearer with an example.

AllowPartialFulfillmentOrder is set to true and the order requests:

  • 4 shoes
  • 2 sweaters
  • 2 scarfs.

SFS sheet has DisableLineSplitting false
Warehouse sheet DisableLineSplitting true

OU:StoreA has 2 shoes and 1 sweater
OU:StoreB has 2 shoes and 1 sweater
WarehouseA has 2 shoes, 1 sweater and 1 scarf
WarehouseB has 2 shoes, 1 sweater and 1 scarf

Since neither the stores nor the warehouses have enough shoes and sweaters on their own, the fulfillment of those two will be carried out by the sheet that allows line splitting: a combination of stores A and B.


Since only the warehouses carry scarfs however, and both only have a single one while splitting is disabled for the warehouse sheet, the entire OrderLine containing scarfs will be *Cancelled*.

UseReplacementProductsForStock​

By creating product relations you can define which products serve as a replacement for other products. That means if one product is out of stock, the other can be used instead.

You can make your order orchestration include replacement stock when calculating fulfillment by using the following option: UseReplacementProductsForStock.


option UseReplacementProductsForStock as true

MaxRetryCountForDeactivatedLines​

The last available option is MaxRetryCountForDeactivatedLines. This option involves orderlines with the Deactivated status and indicates the number of deactivated OrderFulfillmentLines that are allowed to exist for a supplier/sheet combination before that combination is ignored again.


option MaxRetryCountForDeactivatedLines as 5

note

The OrderLine can only be rerouted to the same supplier/sheet a second (or more) time if ALL the existing fulfillment lines for the OrderLine have the status 'Deactivated'. If there are any cancelled fulfillments, because the store actively declined the SFS task for example, they are ignored in future orchestrations.

After stating the fulfillment, action and optionally option properties, it's time to define the scopes.

Custom fields​

A special mention is warranted for the use of custom fields, since these allow you to create custom logic throughout the entirety of your sheets.

They can thus be used in the OrderLine scope for example, allowing you to finetune the logic for specific lines of your order only, or in the Supplier scope to ensure the use of specific suppliers.

The custom fields can live on OUs, orders, etcetera; you can pinpoint to whichever ones you need to use here.

Examples of custom fields usage

In the following example the order contains a line with a product that needs printing, thus requiring an OU with a printing capability.

Explicitly the example entails that when the order line has a custom field called CustomizationType with the value print, then the supplier is required to be an OU with a custom field called 'PrintingCapable' and the value true.

require Supplier.Customfields.PrintingCapable = true when Orderline.Customfields.CustomizationType = β€˜print’

The below example is similar, but a bit more in-depth. It sets the variable CustomizationType with whatever value is passed, or to None when there's no value. It is then used to require the Supplier to be part of the OU set called PrintSuppliers when the custom field called CustomizationType has the value Print.

We even go a bit further by adding the (rejection) reasons, which you'll read about a few chapters down.

with CustomizationType as when [OrderLine.CustomFields.CustomizationType] has value then [OrderLine.CustomFields.CustomizationType] else then 'None' 

require [Supplier.SetNames] in 'PrintSuppliers' when CustomizationType = 'Print'
with reason 'NoPrinting', $'Supplier {[Supplier.Name]} is not part of the set of OUs that print stuff'


Require​

Require allows you to require certain values on existing variables.

Example:

Python
require [Supplier.Type] = 'Shop'

In this example, the sheet negates all possible suppliers that don't have the organization type 'Shop'.

With reason​

In the previous example, possible suppliers are rejected for a certain reason. It's possible to track these rejections using with reason.

Example:

Python
require [Supplier.Type] = 'Shop' with reason 'NotShop'

You can also add a more elaborate description after the reason:

Python
require [Supplier.Type] = 'Shop' with reason 'NotShop', 'This organization unit is not a shop'

This example however, should never really be used. Because it is a bad example. This with reason will log every single rejected organization unit in the database with the reasoning. If your organization is of considerable proportions, this could end up being some pretty heavy logging.

Filter reasons​

It is also possible to limit the amount of returned reasons. Example:

In a ShipFromStore sheet, we only want to work with stores that are within ten kilometers of the customer:

Python
require Supplier.DistanceToShippingAddressInKm < 10 
with reason 'TooFarAway'

This would result in failure reasons for every store that is not within ten kilometers of the shipping address. This information probably isn't relevant for you, so you want to narrow this down. You can do so using the when statement:

Python
require Supplier.DistanceToShippingAddressInKm < 10
with reason 'TooFarAway' when Supplier.DistanceToShippingAddressInKm < 20

Now you will only get failure reasons for stores that are further away than ten kilometers, but are within twenty kilometers of the customer.

With template string​

This is pretty much the same as the detailed description of with reason, the difference being the option to allow for the use of variables.

Example:

Python
require [Supplier.Type] = 'Shop' with reason 'NotShop', $'Organization unit {[Supplier.Name]} is not a shop'

But continue​

In case you want to know the rejection reason for an organization unit at a certain step, but that organization unit is rejected by some logic that comes first, you can use but continue in that initial line. This doesn't un-reject the organization units, it simply takes the organization units that are rejected at a specific line, into account for following lines, until rejected again. Or, well, rejected2.

Example:

Python
require [Supplier.Type] = 'Shop' with reason 'NotShop', 'This organization unit is not a shop' but continue

Data​

In addition to the aforementioned elements of which the usage is completely up to you, both Actions require a mandatory set of data lines.

SHIP_FROM_STORE​

EVA needs the following data to create a Ship-from-Store task:

Python
data [TravelDistanceText] as [Supplier.RouteToShippingAddressByBike.DistanceText]\
data [TravelTimeText] as [Supplier.RouteToShippingAddressByBike.TimeText]\
data [TravelTimeInMinutes] as [Supplier.RouteToShippingAddressByBike.TimeInMinutes]\
data [TravelDistanceInKm] as [Supplier.RouteToShippingAddressByBike.DistanceInKm]

EXPORT_ORDER​

EVA has to know which warehouse to eventually export the order to:

Python
data [ExporterName] as 'WAREHOUSE'

TaskLabels​

By specifying a data called TaskLabels you can influence the labels on the SFS task to distinguish different types of orders. This way you can easily see which order/task is for example a fanout task and which one is an order via another platform. You can then prioritize delivery.

In order to make fanout a label as well, you can enable the setting ShipFromStore:FanoutTaskLabel (defaults to null). When enabled, if the fulfillment is part of a fanout, then that label is added to the list of labels (if any).

Examples of using labels

In the first example, an order comes in via PushSalesOrder. It has a custom field that specifies the order was made via the Popup channel.

Python
"CustomFields": {
"PickupServicePointID": null,
"Channel": "Popup"
}

Now we give this channel a specific task label.

Python
with PopupLabel as (when Order.CustomFields.Channel = 'Popup' then 'Popup' else then nothing)
#nothing is not included in the list, you can also fallback to e.g. webshop as the default

data TaskLabels as AmazonLabel

Here's a more expansive example with multiple task labels.

Python
fulfillment ShipFromStore

action 'SHIP_FROM_STORE'

scope Order

with Random1Label as (when Order.CustomFields.IsRandom1Order = 'true' then 'Random1' else then 'NotRandom1')
with Random2Label as (when Order.CustomFields.IsRandom2Order = 'true' then 'Random2' else then nothing)

scope OrderLine

with IsLimitedEdition as (when Product.Content.is_limited_edition = 'true' then 'LimitedEdition' else then 'NotLimitedEdition')

data TaskLabels as Random1Label, 'SFS_task', Random2Label, IsLimitedEdition

Score​

Used to rate a sheet based on desired fulfillment methods for different variables. The highest scoring sheet or combination of sheet wins.

Example:

Python
score add 1.0 / [Supplier.RouteToShippingAddressByBike.DistanceInKm]

Examples​

Ship-from-store example #1​

Python
fulfillment ShipFromStore

action 'SHIP_FROM_STORE'

# check supplier requirements
scope Supplier

# First filter out anything that isn't a shop
require [Supplier.Type] = 'Shop'

require [Order.ShippingAddress.IsPickupPoint] = false

# require [Order.ShippingAddress.IsPickupPoint] = false with reason 'Order is headed for pickup point, cannot route it to a shop'

# Make sure it's not a closed shop
require [Supplier.Status] <> 'Closed'

with [AvailableStores] as '1187', '1026', '1155', '1109', '3001', '1220'

require [Supplier.BackendID] in [AvailableStores]

with [mondayToSunday] as 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday','Saturday', 'Sunday'

# Only shop 3001 is open on sunday
with [openingDays] as [mondayToSunday]

# The current day (in the time zone of the shop) should be in the opening days of the shop
require [Supplier.LocalTime.DayOfWeek] in [openingDays] with reason 'ShopNotOpenOnDay', $'Shop not open on {[Supplier.LocalTime.DayOfWeek]}' but continue

# Determine from what time (the hour of day) the shop should receive SFS tasks
with [startTime] as 7

# Determine to what time (the hour of the day) the shop should receive SFS tasks
with [endTime] as 23

# Check that the local time of the shop is at least the start time at which it can receive tasks
require [Supplier.LocalTime.Hour] > [startTime] with reason 'ShopNotOpenYet', 'Too early for shop to accept orders' but continue

# Check that the local time of the shop is not after the end time
require [Supplier.LocalTime.Hour] < [endTime] with reason 'ShopAlreadyClosed', 'Too late for shop to accept orders' but continue

# Here we determine the limit for the distance from the shop to the shipping address
with [radiusLimit] as (when [Supplier.Address.City] = 'Amsterdam' then 8
else then 15)

require [Supplier.DistanceToShippingAddressInKm] < [radiusLimit]

require [Supplier.ActiveShipFromStoreTasks] < 300

# The amount of minutes the shop gets to start the pick-task before it gets cancelled
data [DeadlineInMinutes] as 600

# The amount of minutes the shop gets to start the pack-task before it gets cancelled
data PackDeadlineInMinutes as 10

# The amount of minutes the shop gets to start the ship-task before it gets cancelled
data ShipDeadlineInMinutes as 10

# check orderline requirements
scope OrderLine

require [Product.BackendID] <> 'TU410980' with reason 'shop does not support giftcards'

# no shops support giftwrapping right now
require [IsGiftWrapped] = false with reason 'ShipDoesNotDoGiftWrapping', $'Shop does not support giftwrapping' but continue

# Require that the shop has available stock
require [SupplierQuantityAvailable] >= [OrderLine.QuantityOrdered] with reason 'InsufficientStock', 'Shop does not have enough stock available' but continue

# check shipment requirements, this is the collection of lines that we propose the supplier will fulfill
scope Shipment

# the SFS code wants these data variables to exist
data [TravelDistanceText] as [Supplier.RouteToShippingAddressByBike.DistanceText]
data [TravelTimeText] as [Supplier.RouteToShippingAddressByBike.TimeText]
data [TravelTimeInMinutes] as [Supplier.RouteToShippingAddressByBike.TimeInMinutes]
data [TravelDistanceInKm] as [Supplier.RouteToShippingAddressByBike.DistanceInKm]

# we like SFS so we give it a higher score by default, so it 'wins' from warehouse fulfillment
score add 100

# we like it better when a shop is close, so a shop that is 100m away will have 10 added to its score while a shop that is 2km away will have 0.5 added to the score
score add 1.0 / [Supplier.RouteToShippingAddressByBike.DistanceInKm]

# fanout is the concept of 'pooling', creating more SFS tasks for the same order
scope Fanout

# if we already tried to fulfill this order using the SFS sheet before then we want to fan out (= let more than task be created) to at most 5 other stores, otherwise we have a fan out of zero so no other stores get a task
fanout as when [HasExistingFulfillmentLines] then 100 else then 30
Settings for SFS

Remember that there are settings that affect SFS as well.

Ship-from-Store example #2 (with TaskLabels)​

# Beginning of the sheet specifically for IBE logic  
fulfillment IBE

# Reference to backend code (SFS) that is the resulting action if this fulfillment method is used.
action 'SHIP_FROM_STORE'

# Allow partial fulfillment
option AllowPartialFulfillment as true

# Check order level requirements
scope Order

require [Order.ShippingAddress.CountryID] in 'NL', 'BE'

require [Order.SoldFromOrganizationUnit.BackendID] = 'IBE_Company'

require [CurrentFulfillmentAttempts] < 5
with reason 'MaxTries', $'Tried to allocate order {[CurrentFulfillmentAttempts]} already'

require [Order.TotalAmountInTax] >= 10

# Check supplier level requirements
scope Supplier

# Check for Shop type OU only
require [Supplier.Type] = 'Shop'
with reason 'NotAShop', $'Supplier {[Supplier.Name]} is not a shop'

# Check for Supplier type OU only
require [Supplier.Type] = 'Supplier'
with reason 'NotASupplier', $'Supplier {[Supplier.Name]} is not a supplier / SFS enabled store'

# Set the deadline for SFS task this sheet creates as a result (in minutes)
# Orders exported during the weekend
with [Weekend] as 'Sunday'

data [DeadlineInMinutes] as (when [Supplier.LocalTime.DayOfWeek] in [Weekend] then 2880 else then 480)

# only take into account INL stores
require [Supplier.BackendID] contains 'IBE'

# Limit the organizations to the ones in NL, to make this sheet specific.
require [Supplier.CountryID] = 'NL'

# # Proximity requirement
# within the set radius, we prefer the store that is closest to the shipping Adress, using the RouteToShippingAddressByBike.DistanceInKm variable
score add 100 / [Supplier.DistanceToBillingAddressInKm]


## CustomizationType requirement check
#require [Supplier.CustomFields.PrintingCapable] = true # when CustomizationType = 'Print'
# with reason 'NoPrinting', $'Supplier {[Supplier.Name]} is Printing capability set to {[Supplier.CustomFields.PrintingCapable]}, and the value of CustomizationType is {[CustomizationType]} so it is a no no'

# Check order line level requirements
scope OrderLine


# Set a variable 'CustomizationType' with whatever value is passed, or to 'None' when no value
with CustomizationType as when [OrderLine.CustomFields.CustomizationType] has value then [OrderLine.CustomFields.CustomizationType] else then 'None'


# Example 1 with customfield on OU
#require [Supplier.CustomFields.PrintingCapable] = true when CustomizationType = 'Print'
# with reason 'NoPrinting', $'Supplier {[Supplier.Name]} is Printing capability set to {[Supplier.CustomFields.PrintingCapable]}, and the value of CustomizationType is {[CustomizationType]} so it is a no no'

# Example 2 with set
require [Supplier.SetNames] contains 'PrintSuppliers' when CustomizationType = 'Print'
with reason 'NoPrinting', $'Supplier {[Supplier.Name]} is not part of the set of OUs that print stuff'


# Supplier needs stock for this line
require [SupplierQuantityAvailable] >= [OrderLine.QuantityOrdered]
with reason 'NoStock', $'Supplier {[Supplier.Name]} only has {[SupplierQuantityAvailable]} stock available'

# We prefer store fulfilment, so we bump the score of this sheet
score add 20

# define labels
with RandomplatformLabel as (when Order.CustomFields.Channel = 'WEBSHOP' then 'WEBSHOP' else then 'πŸ’» Other Source')
#nothing is not included in the list, you can also fallback to e.g. webshop as the default

# set labels
data TaskLabels as RandomplatformLabel

# set priority
data ShipFromStoreTaskPriority as when Order.CustomFields.Channel = 'WEBSHOP' then 1 else then -1

Warehouse fulfillment​

Python
fulfillment WarehouseFulfillment

action 'EXPORT_ORDER'

scope Supplier


require [Supplier.Type] = 'Warehouse'


with [warehouseCountryID] as when [Order.ShippingAddress.CountryID] in 'GB' then 'GB'
else when [Order.ShippingAddress.CountryID] in 'SE', 'NO', 'DK' then 'SE'
else when [Order.ShippingAddress.CountryID] in 'US'then 'US'
else when [Order.ShippingAddress.CountryID] in 'CN', 'HK' then 'HK'
else then 'NL'
end

require [Supplier.CountryID] = [warehouseCountryID]

data [ExporterName] as when [warehouseCountryID] = 'SE' then 'BONVER'
else then 'ARVATO' end

Returns with order orchestration​

To make returns part of the order orchestration process, a few steps have to be taken, aside from the creation of the new sheet itself. You also have to enable the following setting: UseOrderOrchestrationForReturnOrders.

When creating a new sheet, the default state of the sheet is 'Sales'. To make it into a returns sheet, you have to define it so with the following statement in the top of the sheet:

option OrderIntents as 'Returns'

If you prefer, you can also make an all-purpose sheet by adding all three orchestration possibilities, like so:

option OrderIntents as 'Sales', 'Purchase', 'Returns'

Furthermore, the new sheet needs to make use of the following OrganizationUnitType, which was added for the purpose of returns especially: ReturnsPortal. The fulfillment method EXPORT_RETURN_ORDER was also added for this, which simply does what previously happened before order orchestration, such as creating the return labels/documents and emailing them.

The OU that the return order is orchestrated to, is made available on the ReturnForm stencil template and on the ReturnMail stencil template. The stencil model has a ReturnOrganizationUnit property on its root level, for the 99.99% case where the return order is headed to a single warehouse. However,the fulfillment OU is an OrderLine-level property, so theoretically it’s possible that each orderline is headed to a different OU. The root-level property will always be filled in (at least when using order orchestration) and should be sufficient for all uses. The same property is however also present on each OrderLine. The above goes for both the ReturnForm and the ReturnMail stencil template.

Below you can find an example sheet for returns.

Order orchestration returns sheet
fulfillment ReturnOrdersFromReturnPortal

action 'EXPORT_RETURN_ORDER'

option OrderIntents as 'Returns'

scope Order

require Order.SoldFromOrganizationUnit.Type = 'ReturnsPortal'

scope Supplier

require Supplier.Type = 'Warehouse'