Skip to main content

Creating your sheets

docs image

Creating your sheets

Create your sheets in Admin Suite

By now you should have a good understanding of what order orchestration entails and what kind of sheets you need in your environment. So head on over to the Orchestration chapter in Admin Suite. This is where your editors are available to actually create the sheets.

We do the actual creation with orchestration's Advanced editor. Since we've already gone over all possible scopes and properties you can use, we won't go into those kinds of details here. Instead, we'll give you a headstart on the creation of your own sheets by giving you complete examples of our own sheets.

It should go without saying, but: don't blindly copy the examples. Try and understand what each line does and repeat it in your own environment, if you need it.


Authorization

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

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 '13', '14', '15'

require [Supplier.BackendID] in [AvailableStores]

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

# Only shop 15 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 8

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

# 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 number of minutes the shop gets to start the pick-task before it gets cancelled
data [DeadlineInMinutes] as 180

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

# The number 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] <> '112-114-1' 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 NewBlack logic  
fulfillment NewBlack

# Reference to back-end 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] = 'NewBlack_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 'NewBlack'

# 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 not possible'

# 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 not possible'

# 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 fulfillment, 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

// Alternative: marking it as urgent

with UrgentTask as when [OrderLine.FulfillmentOptions.X] then '🏎 Urgent 🏎'

# set labels
data TaskLabels as RandomplatformLabel

// or, for the alternative

data TaskLabels as UrgentTask

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

// or, for the alternative

data ShipFromStoreTaskPriority as when OrderLine.FulfillmentOptions.X then 2 else then -2

Warehouse fulfillment

The following examples shows you a sheet for warehouse fulfillment, along with its resulting export.


fulfillment WarehouseFulfillment

action 'EXPORT_ORDER'

scope Supplier


require [Supplier.Type] = 'Warehouse'


with [warehouseCountryID] as when [Order.ShippingAddress.CountryID] in 'ES' then 'ES'
else when [Order.ShippingAddress.CountryID] in 'SE', 'NO', 'DK' then 'SE'
else when [Order.ShippingAddress.CountryID] in 'US', 'ME', 'CA' then 'US'
else then 'NL'
end

require [Supplier.CountryID] = [warehouseCountryID]

data [ExporterName] as when [warehouseCountryID] = 'US' then 'NB-US-Shipping'
else then 'NB-EU-Shipping' end

{
"Order": {
"ID": "string",
"WarehouseID": "string",
"GlobalID": "string",
"BackendSystemID": "string",
"BackendID": "string",
"Type": "string",
"BackendType": "string",
"Remark": "string",
"AllowPartialFulfillment": false,
"CustomFields": {
"IsBackFillOrder": false,
"CF_PaymentMethod": "string",
"CF_PaymentReference": "string",
"CF_ShippingMethod": "string",
},
"OrderReference": "string",
"OrderFulfillmentID": "string",
"CustomerOrderID": "string",
"TotalAmountInTax": 1.1,
"ShippingCosts": 0.0,
"CurrencyID": "string",
"Customer": {
"ID": "string",
"BackendID": null,
"FirstName": "string",
"LastName": "string",
"FullName": "string",
"Telephone": "string",
"Email": "string",
"BackendRelationID": null,
"Initials": null,
"DateOfBirth": null,
"Gender": null,
"PlaceOfBirth": null,
"LanguageID": "string",
"CountryID": "string",
"Nickname": null,
"FiscalID": null,
"PhoneNumber": "string",
"TimeZone": null,
"BackendSystemID": "string",
"GlobalID": null,
"Title": null,
"Salutation": null
},
"ShippingAddress": {
"AddressedTo": "string",
"FirstName": "string",
"LastName": "string",
"HouseNumber": "string",
"Address1": "string",
"Address2": null,
"ZipCode": "string",
"City": "string",
"State": null,
"CountryID": "string",
"Street": "string"
},
"BillingAddress": {
"AddressedTo": "string",
"FirstName": "string",
"LastName": "string",
"HouseNumber": "string",
"Address1": "string",
"Address2": null,
"ZipCode": "string",
"City": "string",
"State": null,
"CountryID": "string",
"Street": "string"
},
"FallbackShippingAddress": null,
"SoldFrom": {
"ID": "string",
"BackendID": "string",
"Name": "string",
"BackendRelationID": "string"
},
"OriginatingFrom": {
"ID": "string",
"BackendID": "CC_NL",
"Name": "string",
"BackendRelationID": "string"
},
"ShipFrom": {
"ID": 135,
"BackendID": "C01-MAIN01",
"Name": "E-Com Warehouse",
"BackendRelationID": "C01-MAIN01"
},
"InvoiceTo": {
"ID": "string",
"BackendID": "string",
"Name": "string",
"BackendRelationID": "string"
},
"Lines": [
{
"ID": "string",
"BackendID": "string",
"ProductID": "string",
"SupplierProductID": null,
"Description": "string",
"Quantity": 1,
"UnitPrice": 1.1,
"TaxRate": 1.210000,
"RequestedDate": "2023-06-30T00:00:00",
"ShippingMethod": "string",
"Carrier": "string",
"Barcode": "string",
"SupplierProductDescription": null
}
]
},
"Url": "https://..."
}

Preview your Orchestration

Once you've got your orchestration sheets all set up, you can check how they would work in practice.

To do so: click the 'scroll' icon next to the '+' in the main overview to open up your orchestration previewer. This lets you specify any existing order number, 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.

Current stock only

We do not consider historical or future stock for the preview.

It is only meant to bypass the opening hours check. You should consider this merely a test of your sheets and not as a means of checking alternative orchestration paths for historic orders.


Orchestration results in detail

Aside from checking the probable result of the order orchestration by means of the preview option, you can also check the actual orchestration results in detail. These details include:

  • Fulfillments that resulted from the orchestration (can be multiple);
  • The shipments each fulfillment contains;
  • Where each shipment gets its score from;
  • Which order lines 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.

All this information is stored in a blob, which is attached to the OrderFulfillment itself. If there is fanout 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.

Tasks, cancellations and recalculation

Oncse an order has successfully gone through orchestration and its result is Ship from Store, then corresponding SFS tasks will be created for all involved stores.

Delay task creation

If you want to, you can offer your consumers a custom time period to change their mind before you start the actual fulfillment proces. By setting OrderFulfillment:ExportDelayInMinutes with your custom value, the actual export (and tasks creation) will be delayed by that time. This does not affect the fulfillment result nor the commits on stock.

During this "cooldown" period, only order (line) cancellations are possible. Afterwards the possibility of modifications and/or cancellations on exported orders are determined based on your scripts in the AllowOrderLineModificationDuringFulfillment extension point.

Depending on the store's Ship from Store settings, the store may be allowed to refuse picking certain order lines. If not all order lines are picked in their SFS task, then the unpicked lines will return to the orchestration and the next best path for those specific lines will be calculated - while excluding the first store.

Recalculation is for Deactivated tasks only

Whether an OrderLineFulfillmentLine has a Cancelled or Deactivated state, is a consequence of how a store handled it when it was first offered to the store. These two states have a big impact on how the store is treated in case of recalculations.

When order lines are run through orchestration for a second (or third) time, the order lines contained in an OrderFulfillmentLine that got Cancelled by a particular store will skip that same store during recalculation.

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 number of times as specified in the MaxRetryCountForDeactivatedLines option.

OrderFulfillmentLine states

The OrderFulfillmentLine can consequently have five different states, depending 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

Reasons displayed in Order history

The Cancelled and Deactivated status we just discussed here above can be reached for various reasons, based on how the SFS task was handled by the store. For the sake of clarity, here's a collection of possible reasons which are returned to you in the Order history tab of an order.

  • NULL: meaning no cancellation reason available / applicable
  • Otherstore: deactivated during fan out because another store has picked up the task already
  • FulfillmentCancelled: the order line 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
  • Manual: manually cancelled by a user
  • ​Expired: deadline ran out so we cancelled the task