How to Create an Order Programmatically in WooCommerce
WooCommerce is by far the most popular plugin for eCommerce for WordPress websites. And rightfully so. It does a great job of handling the cart and checkout process, not to mention calculating taxes and managing orders. All this is great, but sometimes you may need to create an order on the fly without going through the entire checkout process, tediously adding products to the cart before checking out. The good news is that WooCommerce has a function that we can use in order to create an order: wc_create_order().
Prerequisites
- A basic understanding of PHP
- A self-hosted WordPress Site
- The WooCommerce plugin installed with some published products
Create the Order
To begin, we’ll first create our function to handle the order process. For the sake of of this post, I’ll call it quick_order(), but you can call it whatever you like. We’ll then use the built-in wc_create_order() function. The wc_create_order() function takes some optional arguments:
- status
- customer_id
- customer_note
- parent
- created_via
- cart_hash
- order_id
While none of these are required, keep in mind that if you choose not to provide a customer ID, the order will be allocated to a guest account. For this post, I’ll assign the order to the user initiating the order, ie. the logged in user. We can do this using the WordPress function get_current_user_id().
<?php
function quick_order() {
$user_id = get_current_user_id();
$args = array(
'customer_id' => $user_id,
// The other options
//'status' => null,
//'customer_note' => null,
//'parent' => null,
//'created_via' => null,
//'cart_hash' => null,
//'order_id' => 0,
);
// Create the order and assign it to the current user
$new_order = wc_create_order($args);
}
?>
Add Products
Now that our order exists, we can add some products and later, update the order meta. This is done by using the add_product() method which accepts three parameters, but we’ll only need two: product id and quantity.
My little trick to make things easier is to store the product/variable ids into and associative array. Our goal is to have an array that looks something like this:
$products_to_add = array(
//'id' => 'quantity',
'1234' => 4,
'5678' => 2,
);
Once I have that array, it’s plain sailing from there.
Before you add the products, it’s good to understand that if your products are variable ones, you will need the IDs of those particular variable products. If you use the parent product ID, your order will be inaccurate, especially if your variations have different prices. For simple products, you just need the product ID.
For my example, I’m going to use an array of products I’ve imported from a CSV.
$imported_order_items = get_csv_imports();
Visually, my import will look something like this:
$imported_order_items = array(
'product_1' => array(
'SKU' => 'test-1',
'qty' => 2,
'size' => 'S', // Shirt Size comes in S-XXL
'colour' => 'Black',
),
'product_2' => array(
'SKU' => 'test-1',
'qty' => 10,
'size' => 'S', // Shirt Size comes in S-XXL
'colour' => 'Black',
),
'product_3' => array(
'SKU' => 'HFS-MER04',
'qty' => 3,
'size' => '28', // Blouse Size comes in 28-60
),
'product_4' => array(
'SKU' => 'HFS-GLOVES',
'qty' => 1,
),
'product_5' => array(
'SKU' => 'HFS-MER03',
'qty' => 3,
'size' => '30',
)
);
Now comes the tricky part. You may have noticed there are a few differences between the items. Product 4 is a simple product and pretty easy to add, however, the others are variable products and can pose a challenge, especially if there’s more than one attribute that defines each variation like Product 1 and 2.
Another challenge I faced in this particular instance, is that my clients .csv file has a general “size” attribute although shirts come in sizes Small to XX Large and blouses come in sizes 28 to 60 stored as attributes “shirt-size” and “blouse_size” respectively.
These are factors I had to consider, but you may have similar aspects of your products you’ll need to keep in mind.
Let’s continue…
Find WooCommerce Variable Products
Getting the variation IDs seems somewhat complicated, but I’ll explain my process. If you know of a better way to do this, please feel free to let me know in the comments below. Here’s the full code for obtaining the variation ids. A good idea would be to place this in a function, but I won’t cover that in this post.
// Define the array that will store our ids and quantities
$products_to_add = array();
// Loop through the $imported_order_items array to check if it's simple or variable
foreach ($imported_order_items as $item) {
// get the product id and define the $product variable
$id = wc_get_product_id_by_sku($item['SKU']);
$product = wc_get_product( $id );
// Check if product is variable
if ($product->is_type('variable')) {
$available_variations = $product->get_available_variations();
foreach ($available_variations as $variation) {
$variation_id = $variation['variation_id'];
$var_product = wc_get_product($variation_id);
$atts = $var_product->get_attributes();
// Conditional to check for variations that are varied by both size and colour
if (isset($atts['pa_shirt-size']) && $atts['pa_shirt-size'] === strtolower($item['size']) && isset($atts['pa_colour']) && $atts['pa_colour'] === strtolower($item['colour']) ) {
// check if the key exists and add to the value, if not we'll define it
if (!array_key_exists($variation_id, $products_to_add)) {
$products_to_add[$variation_id] = $item['qty'];
}
else {
$products_to_add[$variation_id] += $item['qty'];
}
}
// only comes in shirt size variations, so we don't look for colour
else if (isset($atts['pa_shirt-size']) && $atts['pa_shirt-size'] === strtolower($item['size']) && !isset($atts['pa_colour']) ) {
// check if the key exists and add to the value, if not we'll define it
if (!array_key_exists($variation_id, $products_to_add)) {
$products_to_add[$variation_id] = $item['qty'];
}
else {
$products_to_add[$variation_id] += $item['qty'];
}
}
// only comes in blouse size variations
else if (isset($atts['pa_blouse_size']) && $atts['pa_blouse_size'] === strtolower($item['size']) ) {
// check if the key exists and add to the value, if not we'll define it
if (!array_key_exists($variation_id, $products_to_add)) {
$products_to_add[$variation_id] = $item['qty'];
}
else {
$products_to_add[$variation_id] += $item['qty'];
}
}
} // end $available_variations foreach loop
}
// if its a simple product, we can add it more easily
else {
$products_to_add[$product->get_id()] = $item['qty'];
}
}
What This Code Does
First, I’ve defined the $products_to_add array that will store my product and variation IDs once I’m able to obtain them.
Secondly, I’m going to loop through the imported items using a foreach loop: foreach ( $imported_order_items as $item ) {} .
Next, inside the imported items loop, I’m going to check if a product is variable using $product->is_type(‘variable’). is_type() is a WooCommerce method which returns the type of product, for example, ‘simple’, ‘variable’, ‘virtual’ etc.
If it is a variable product, I’m going to get all the variations for that product so I have something to compare to. I’m going to use the get_available_variations() method. One drawback with get_available_variations() is that, depending on the amount of variations you have, it may cause performance issues. I haven’t had any issues, but I’ve only had a maximum of 19 variations for now. Let me know of your experience in the comments below.
With all the variations stored in the $available_variations, I loop through the array to compare the attributes of the products I’ve imported the those of the products that are stored in the database. To get an array the product’s attributes, we have to use the get_attributes() method, which I’ve stored in $atts.
Now, the if statement is going to filter those products that match all my queried attributes. First I check if the key for the item’s attribute using isset($atts[‘pa_shirt-size’]) and compare the size in the imported product to the value of ‘pa_shirt-size’. I do the same for ‘pa_colour’.
Then I run else if statements for all attribute combinations I’m looking for. If the product only has one variable attribute, like the ‘blouse_size’, I just check if there is an array key using isset($atts[‘pa_shirt-size’]) and compare it to the value in the import $atts[‘pa_blouse_size’] === strtolower($item[‘size’]) ) . I’m using PHP’s built-in strtolower() function to convert the import text to lower case since WooCommerce stores attribute slugs in lowercase.
Within each if … if else statement, I’ll push my product and quantity to the $products_to_add array. Because I’m storing the IDs as keys, and an array’s keys have to be unique, I first check if the array key exists using another built-on PHP function: array_key_exists(). This will first check if the loop has already set the key. If it hasn’t I’ll set it, but if it has we’ll add the quantity to the previous quantity. If we don’t this, only the last product with that variation id will be in array because it will overwrite the previous value.
// If the variation ID key doesn't already exist, let's create it
if (!array_key_exists($variation_id, $products_to_add)) {
$products_to_add[$variation_id] = $item['qty'];
}
// if the variation ID key exists, add the quantity to previous value in the loop
else {
$products_to_add[$variation_id] += $item['qty'];
}
We just need to include the else statement to include simple products:
if ($product->is_type('variable')) {
...
}
// if its a simple product, we can add it more easily
else {
$products_to_add[$product->get_id()] = $item['qty'];
}
A print_r() outside of the $imported_order_items foreach loop will give us this:
Array (
[14321] => 12
[10739] => 3
[10760] => 1
[10722] => 3
)
And that’s exactly what I’m looking for!
Now we can easily add our products to the order. We’ll use the add_products method I mentioned earlier while looping through the $products_to_add array, using the key/value pairs as our product and quantity. The wc_get_product() WooCommerce function will get all the necessary information from the product.
// Add the products to the order
foreach ($products_to_add as $product => $qty) {
$new_order->add_product( wc_get_product($product), $qty );
}
Update the Order Meta
Next we can optionally set the shipping and billing address for the order.
$billing_address = array(
'first_name' => get_user_meta( $user_id, 'first_name', true ),
'last_name' => get_user_meta( $user_id, 'last_name', true ),
'company' => get_user_meta( $user_id, 'billing_company', true ),
'email' => get_user_meta( $user_id, 'billing_email', true ),
'phone' => get_user_meta($user_id, 'billing_phone', true ),
'address_1' => get_user_meta( $user_id, 'billing_address_1', true ),
'address_2' => get_user_meta( $user_id, 'billing_address_2', true ),
'city' => get_user_meta( $user_id, 'billing_city', true ),
'state' => get_user_meta( $user_id, 'billing_state', true ),
'postcode' => get_user_meta( $user_id, 'billing_postcode', true ),
'country' => get_user_meta( $user_id, 'billing_country', true )
);
$new_order->set_address( $billing_address, 'billing' );
//$new_order->set_address( $billing_address, 'shipping' );
My client’s orders have a Cost Centre Number custom meta field. If your orders have custom meta, you can update the meta using the update_meta_data() method and input the meta key and meta value as parameters.
// if your orders have custom meta fields, you can update them here
$new_order->update_meta_data('_billing_cost_centre_number', 'XYZ');
Finalising the Order
With all of the hard work done, we can wrap things up by setting a payment method, calculating the totals, updating the order status and saving the order.
// if your orders have custom meta fields, you can update them here
$new_order->update_meta_data('_billing_cost_centre_number', 'XYZ');
// Set payment gateways
$gateways = WC()->payment_gateways->payment_gateways();
$new_order->set_payment_method( $gateways['bacs'] );
// Let WooCommerce calculate the totals
$new_order->calculate_totals();
// Update the status and add a note
$new_order->update_status('completed', 'Order added programmatically!', true);
// Save
$new_order->save();
And that’s pretty much it. Depending on how many products you have, this could take some time to run. A good idea would be to paste all of the code into a function and call the PHP ini_set(max_exection_time) or set_time_limit() functions to allow your script to complete without making permanent changes to your server configuration. You’ll definitely want to prevent multiple form submissions to prevent the user from submitting the order while the script runs.
Here’s the full code. Let me know if it could be refined, or if some of it needs further explaining.
<?php
function quick_order() {
$user_id = get_current_user_id();
$args = array(
'customer_id' => $user_id,
);
// Create the order and assign it to the current user
$new_order = wc_create_order($args);
$imported_order_items = get_csv_imports();
$products_to_add = array();
foreach ($imported_order_items as $item) {
$id = wc_get_product_id_by_sku($item['SKU']);
$product = wc_get_product( $id );
// Check if product is variable
if ($product->is_type('variable')) {
$available_variations = $product->get_available_variations();
foreach ($available_variations as $variation) {
$variation_id = $variation['variation_id'];
$var_product = wc_get_product($variation_id);
$atts = $var_product->get_attributes();
// Conditional to check for variations that are varied by both size and colour
if (isset($atts['pa_shirt-size']) && $atts['pa_shirt-size'] === strtolower($item['size']) && isset($atts['pa_colour']) && $atts['pa_colour'] === strtolower($item['colour']) ) {
// check if the key exists and add to the value, if not we'll define it
if (!array_key_exists($variation_id, $products_to_add)) {
$products_to_add[$variation_id] = $item['qty'];
}
else {
$products_to_add[$variation_id] += $item['qty'];
}
}
// only comes in shirt size variations, so we don't look for colour
else if (isset($atts['pa_shirt-size']) && $atts['pa_shirt-size'] === strtolower($item['size']) && !isset($atts['pa_colour']) ) {
// check if the key exists and add to the value, if not we'll define it
if (!array_key_exists($variation_id, $products_to_add)) {
$products_to_add[$variation_id] = $item['qty'];
}
else {
$products_to_add[$variation_id] += $item['qty'];
}
}
// only comes in blouse size variations
else if (isset($atts['pa_blouse_size']) && $atts['pa_blouse_size'] === strtolower($item['size']) ) {
// check if the key exists and add to the value, if not we'll define it
if (!array_key_exists($variation_id, $products_to_add)) {
$products_to_add[$variation_id] = $item['qty'];
}
else {
$products_to_add[$variation_id] += $item['qty'];
}
}
} // end $available_variations foreach loop
}
// if its a simple product, we can add it more easily
else {
$products_to_add[$product->get_id()] = $item['qty'];
}
}
// Add the products to the order
foreach ($products_to_add as $product => $qty) {
$new_order->add_product( wc_get_product($product), $qty );
}
// Next we update the address details
$billing_address = array(
'first_name' => get_user_meta( $user_id, 'first_name', true ),
'last_name' => get_user_meta( $user_id, 'last_name', true ),
'company' => get_user_meta( $user_id, 'billing_company', true ),
'email' => get_user_meta( $user_id, 'billing_email', true ),
'phone' => get_user_meta($user_id, 'billing_phone', true ),
'address_1' => get_user_meta( $user_id, 'billing_address_1', true ),
'address_2' => get_user_meta( $user_id, 'billing_address_2', true ),
'city' => get_user_meta( $user_id, 'billing_city', true ),
'state' => get_user_meta( $user_id, 'billing_state', true ),
'postcode' => get_user_meta( $user_id, 'billing_postcode', true ),
'country' => get_user_meta( $user_id, 'billing_country', true )
);
$new_order->set_address( $billing_address, 'billing' );
// if your orders have custom meta fields, you can update them here
$new_order->update_meta_data('_billing_cost_centre_number', 'XYZ');
// Set payment gateways
$gateways = WC()->payment_gateways->payment_gateways();
$new_order->set_payment_method( $gateways['bacs'] );
// Let WooCommerce calculate the totals
$new_order->calculate_totals();
// Update the status and add a note
$new_order->update_status('completed', 'Order added programmatically!', true);
// Save
$new_order->save();
}
Where To Use This Code
Copy and paste the code snippet(s) into your child theme’s functions.php file, or better yet, paste it into a custom plugin that handles all your customisations. Then call the function anywhere you’d like to invoke it.
Code References
PHP
WooCommerce
- wc_create_order()
- add_product()
- wc_get_product()
- is_type()
- get_available_variations()
- get_attributes()
- wc_get_product_id_by_sku()
- get_id()
- update_meta_data()
- payment_gateways()
- set_payment_method()
- calculate_totals()
- update_status()
- save()
WordPress
Is this still valid in 2024? Please let me know in the comments below.