Creating Purchase Orders

Inside this guide:
Introduction
Example order
Prerequisites
Mapping to the mutation input
Performing the mutation
Dealing with errors
Next steps

Introduction

To follow this guide, please ensure you are able to call the Zencargo API either using our API Console or in the HTTP client of your choice. See the Making your first call series of guides for how to do this.

You should also be familiar with the Concepts and how Zencargo models Purchase Orders.

The Concepts guide covers off exactly how Zencargo models Purchase Orders and Products. Please read that before starting. The scope of this guide is only to show you how to create Purchase Orders inside Zencargo and which references you're likely going to store inside your own system, we'll tackle how to update or query for PO information in later guides.

The most likely use case that will trigger this action is a new Purchase Order being raised with your manufacturer. If you have a process whereby orders are created and then subsequently confirmed before production may begin (for instance, if your order is created before the price or timelines are agreed) then Zencargo only needs to know about the Purchase Order after it has been confirmed.

Ideally, Purchase Orders should be created in Zencargo as soon as the original expected quantities, timelines and costs are agreed with the manufacturer. This is so that we can effectively track changes to these as production begins, and also so that we can plan and book space for the transportation of the goods.

Depending on your ERP, you may choose to trigger this action when the PO is confirmed, or you may trigger it immediately upon creation of the Purchase Order in your ERP. You will need to perform one mutation, called PurchaseOrdersCreateOrder. To subsequently query this Purchase Order later, you will likely also need to persist some of Zencargo's generated references when the object is created.

Example Order

Throughout the guide, we'll refer to an example order from an imaginary company: The Bluth Company imports goods to the UK. On the 1st January 2021, they raise an order, "PO-SEAWARD-2", with their manufacturer. The order is for:

The merchandising team need both items to deliver into their warehouse in the UK by 4th May 2021. The goods are expected to ship by sea from their manufacturer's warehouse in China to the UK.

In order to make it aboard the sailing that arrives in time, the manufacturer agrees to have the goods ready for pickup on 16th March 2021.

First, let's look at what associated objects need to be created before we can create this order.

Prerequisites

Products

You need to have already created Products in whichever environment you use. You can see an example of how to do this in the Mutations guide or you can refer directly to the ProductsCreateProduct mutation. Ensure that you create two Products, representing Frozen Bananas and Beads (or substitutes for whatever you like).

Networks Locations

You also need to create two Network Organisation Locations, one for the UK warehouse and one for the China Warehouse. It's not currently possible to create Network Locations via API, primarily because nearly all of Zencargo's customers already have existing locations set up when they begin their integrations.

To set up Network Organisation Locations, you need to navigate to the environment you're using to follow this guide and log in to Zencargo. Once you're inside, assuming you want to create a new location:

Repeat these steps twice, once for a UK address and once for a Chinese address. If you already have organisation locations, you can just search for the organisation, choose a location, and copy down the ID that way.

Note: To go live with Zencargo, you'll need to maintain a mapping of Zencargo's Network Organisation Location IDs with however you store these objects in your ERP. You can query for these IDs using the NetworksOrgLoc object. Ask your Zencargo Account Manager for more information. For now, we'll just assume you are hardcoding the IDs we just copied down.

ERP Line Item Id

As you will see later in this guide, you will be able to create a purchase order and specify the ID of the line item as you store it in your ERP system. You will be able to do that with the erp_line_id field.

Mapping to the mutation input

To create this order in Zencargo, we will need to perform the PurchaseOrdersCreateOrder mutation. As we can see in the docs, this mutation takes an object specified as CreateOrderInput and one of the fields in the input type (alongside your accountID) is the PurchaseOrderInput object. Let's look at how the information we just described maps to this object:

{
  "orderReferenceNumber": "PO-SEAWARD-2",
  "orderDate": "2021-01-01",
  "manufacturerID": "uuid12345",
  "originID": "uuid12345",
  "destinationID": "uuid98765",
  "orderedLineItems": [
    {
      "productSku": "10000",
      "quantityOrdered": 10000,
      "cbm": 32.0,
      "initialCargoReadyDate": "2020-03-16",
      "requiredDeliveryDate": "2020-05-04",
      "erpLineId": "YOUR_ERP_LINE_ID",
    },
    {
      "productSku": "20000",
      "quantityOrdered": 50000,
      "cbm": 38.0,
      "initialCargoReadyDate": "2020-03-16",
      "requiredDeliveryDate": "2020-05-04",
      "erpLineId": "YOUR_ERP_LINE_ID",
    }
  ]
}

Note: In our example the goods are being manufactured and collected from the same Organisation Location, so we'll send the same Organisation Location ID to both the manufacturerID and originID fields. If you are manufacturing goods in a separate location to the warehouse they are being picked up from, for instance a CFS, you should discuss with your business stakeholders which locations should be used.

We're omitting totalCost in our example, but you can include this in order to see spend analytics in Zencargo.

Performing the mutation

The easiest way to follow along with this guide is by using the API Console, where you'll be able to copy and paste the example code without having to worry about authentication or formatting. If you're using a different HTTP client, make sure you format the GraphQL correctly.

Defining the variables

Copy and paste the block below into the query variables section of the API console, substituting the "accountUuid", "manufacturerId", "originId", "destinationId" and "erpLineId" fields for your own values. Don't worry if the linter complains about anything else, we'll fix those later.

Variables

{
  "input": {
    "accountUuid": YOUR_ACCOUNT_UUID,
    "purchaseOrder": {
      "orderReferenceNumber": "PO-SEAWARD-2",
      "orderDate": "2021-01-01",
      "manufacturerId": "uuid12345",
      "originId": "uuid12345",
      "destinationId": "uuid98765",
      "orderedLineItems": [
        {
          "productSku": "10000",
          "quantityOrdered": 10000,
          "cbm": 32.0,
          "initialCargoReadyDate": "2020-03-16",
          "requiredDeliveryDate": "2020-05-04",
          "erpLineId": "YOUR_ERP_LINE_ID",
        },
        {
          "productSku": "20000",
          "quantityOrdered": 50000,
          "cbm": 38.0,
          "initialCargoReadyDate": "2020-03-16",
          "requiredDeliveryDate": "2020-05-04",
          "erpLineId": "YOUR_ERP_LINE_ID",
        }
      ]
    }
  }
}

Now that we have our variables, let's look at how to construct the mutation.

Constructing the mutation and input

We'll need to define the mutation and input types. As we discussed previously, the mutation is called PurchaseOrdersCreateOrder and the input type is CreateOrderInput

Paste the code below into the query section (the one at the top in the console) but don't try to execute the mutation. Again, the linter will complain that our return values are empty, but we'll fix that later.

mutation ($input: CreateOrderInput!) { 
  purchaseOrdersCreateOrder(input: $input) { 

  } 
}

This code defines an operation type (a mutation) and defines the type of the variable we'll supply, $input and defines its type should be CreateOrderInput. We then define the mutation name and how the input should be passed into this mutation - in our case we just pass our $input variable as input:.

Next, we'll need to define what we want to return

Defining return values

Looking into the documentation of PurchaseOrdersCreateOrder we can see that we have access to three fields on the return value. We're not interested in clientMutationId and we'll cover off errors later, so for now let's explore the final return field, which is a PurchaseOrder object representing the entity created inside Zencargo.

At the top level of this object, we can see that Zencargo has persisted our order date and order reference number, but has also created a unique id. You may need to persist this Zencargo ID in your ERP in order to query for the PO later, so let's include this in our return value.

Don't paste this now, we'll paste it in one go when we're ready to run the mutation.

mutation ($input: CreateOrderInput!) { 
  purchaseOrdersCreateOrder(input: $input) { 
    purchaseOrder {
      id
      orderReferenceNumber
    }
  } 
}

As you can see, the return value does not include the orderDate that we passed in as a variable, even though it's defined as an option in the docs. This is a common misconception to those new to GraphQL but represents a key benefit of a GraphQL API: you can request only data that you need. In this case, we're trusting that Zencargo has stored the order date we provided in the input correctly (see the errors section below for how to be sure of this) so we don't need to request it back.

Now let's look at the OrderedLineItem objects that we can access:

purchaseOrder {
  ...
  orderedLineItems {
    product {
      skuCode
    }
    quantityOrdered
    id
  }
}

An OrderedLineItem probably maps closely to a line in Purchase Orders in your ERP. One difference is that we can retrieve the whole Product object if we want to, but we just want the skuCode.

We passed in a string called "productSku" in our Input, but we're requesting a skuCode here. Why? The input was in a different context (an ordered line item input) to our return value (a product). Inputs often have different names for fields than the return values because they live in different contexts. Make sure you check the documentation for inputs to adhere to input schemas.

Outside of the Product, we can retrieve the quantityOrdered, and we could also retrieve the initialCargoReadyDate or requiredDeliveryDate alongside other fields. We're omitting these from our return values because we don't need to know the data that we've sent over - we can use the Errors for this. GraphQL is incredibly powerful because it allows you only to retrieve information you need.

We have also requested the OrderedLineItem's id, which is unique to the line on the PO and generated by Zencargo. Many clients store this alongside the PO line in your ERP so that you can track changes to this or query specifically for it later.

Let's look at the final object available to us, and its the first where Zencargo is likely to be the source of truth for your information: the Lot.

purchaseOrder {
  ...
  orderedLineItems {
    ...
    lots {
      id
    }
  }
}

Inside the OrderedLineItem, there's a field, Lots which contains an array of Lot objects.

When you create a PurchaseOrder, a single Lot will be generated for every OrderedLineItem. This Lot may be edited later, either via API or inside Zencargo's UI, and new Lots associated with the same OrderedLineItem may be created later. You can explore how to interact with this in other guides.

There's a detailed explanation of the difference inside the Concepts guide which can be summarised like this:

To put it another way:

Some information on the Lot equates to updated information from the OrderedLineItem, like the estimatedDeliveryDate or the cargoReadyDate (which are respectively more accurate versions of the requiredDeliveryDate and initialCargoReadyDates we passed in to our input for the OrderedLineItem). However, because this is getting requested a few ms after the PurchaseOrder is created, it's likely that these fields will be identical to the input fields. We won't request these in this operation, but we'll cover that in the Querying Purchase Orders guide.

What we will request here, however, is the id of the Lot, which is generated by Zencargo. Many clients store this alongside or instead of the OrderedLineItem ID in their ERP, depending on how your system is set up (you can be sure that an OrderedLineItem corresponds 1-to-1 with your lines, but the relationship between OrderedLineItems and Lots is One-To-Many). You can query for a Lot ID at any subsequent point, but it may be useful to request this field in the mutation so you can store it when you create it in Zencargo.

You can read the Concepts guide for a rundown on how this will be used, and there are other guides covering off how to query and update Lots. For now, let's look at performing this operation.

The final query syntax

We're now ready to run our mutation. If you've been following along, paste the GraphQL below into the query section, inside the brackets already inside. Linting errors should now disappear.

    purchaseOrder {
      id
      orderReferenceNumber
      orderedLineItems {
        product {
          skuCode
        }
        quantityOrdered
        id
        lots {
          id
        }
      }
    }

You're now ready to run the mutation. As a refresher, or if you haven't been following along, here's what we're going to send (remember to replace the variables for account, manufacturer, origin and seller ID)

POST /graphql
Query

mutation ($input: CreateOrderInput!) { 
  purchaseOrdersCreateOrder(input: $input) { 
    purchaseOrder {
      id
      orderReferenceNumber
      orderedLineItems {
        product {
          skuCode
        }
        quantityOrdered
        id
        lots {
          id
        }
      }
    }
  } 
}

Variables

{
  "input": {
    "accountUuid": YOUR_ACCOUNT_ID,
    "purchaseOrder": {
      "orderReferenceNumber": "PO-SEAWARD-2",
      "orderDate": "2021-01-01",
      "manufacturerId": "uuid12345",
      "originId": "uuid12345",
      "destinationId": "uuid98765",
      "orderedLineItems": [
        {
          "productSku": "10000",
          "quantityOrdered": 10000,
          "cbm": 32.0,
          "initialCargoReadyDate": "2020-03-16",
          "requiredDeliveryDate": "2020-05-04"
        },
        {
          "productSku": "20000",
          "quantityOrdered": 50000,
          "cbm": 38.0,
          "initialCargoReadyDate": "2020-03-16",
          "requiredDeliveryDate": "2020-05-04"
        }
      ]
    }
  }
}

Assuming you got the IDs and Product codes valid, you should get back this response, which mimics the body of the response you'd get in a real HTTP request.

JSON response

{
  "data": {
    "purchaseOrdersCreateOrder": {
      "purchaseOrder": {
        "id": "889b9134-433c-4a83-b7cc-d5525f2b1bbb",
        "orderReferenceNumber": "PO-SEAWARD-2",
        "orderedLineItems": [
          {
            "id": "faef6f79-f755-4509-a674-430401aac5e6",
            "lots": [
              {
                "id": "baea52f7-9e39-49b2-9e17-dee9840f2e5e"
              }
            ],
            "product": {
              "skuCode": "10000"
            },
            "quantityOrdered": 10000
          },
          {
            "id": "3a1bcdfc-f80b-4781-9884-2876d76f15b3",
            "lots": [
              {
                "id": "15257312-c592-4417-bdf0-a1a12d6edb0c"
              }
            ],
            "product": {
              "skuCode": "20000"
            },
            "quantityOrdered": 50000
          }
        ]
      }
    }
  }
}

Congratulations! You've created your first Purchase Order inside Zencargo. Looking over the data we got back it's worth reiterating a point: you're getting back only the data you requested, not what was created as you might expect from another type of API.

One thing we're not requesting is any errors. This call succeeded, but that won't always be the case.

Let's finish our guide with the final part: how to uncover errors.

Dealing with errors in your mutations

By now, you're familiar with the idea that you can request what you'd like from the return value of the mutation. As you can see from our PurchaseOrdersCreateOrder mutation documentation, we are able to retrieve an errors field. This is a GraphQL standard that will be available on every mutation on the Zencargo API.

In real requests and responses, there are two places that errors can be returned by Zencargo:

{
  "errors": [
    "errors will be found in here"
  ],
  "data" {
    "NameOfTheOperation": {
      "errors": [
        "errors will also be found in here"
        ]
    }
  }
}

NOTE: the API console only allows for you to introspect the "data" property of the response. You can trigger errors at the top level by using another HTTP client.

How you handle errors is entirely down to you. You can consult the official GraphQL docs or literature online for different strategies. For now, we'll focus on one specific use case: an error when you try to send a mutation containing invalid data.

In your API console, leave the variables data the same, but add an errors object in the query, in the top level.

purchaseOrdersCreateOrder(input: $input) { 
    errors {
      path
      message
    }
    purchaseOrder {
      ...
    }
}

Run this mutation again, with the same variables, you'll get back this response

JSON Response

{
  "data": {
    "purchaseOrdersCreateOrder": {
      "errors": [
        {
          "message": "has already been taken",
          "path": "orderReferenceNumber"
        }
      ],
      "purchaseOrder": null
    }
  }
}

As you can see, we tried to create a second Purchase Order with a duplicate orderReferenceNumber and we got back an errors array containing objects. These objects tell us the path and message. You can use these, if you like, to inform your retry strategy.

This ends our guide designed to help you create Purchase Orders.

Next Steps