Create an order programmatically with line items in Woocommerce 3+

With latest version of WooCommerce is possible try this as something like

$address = array(
            'first_name' => 'Fresher',
            'last_name'  => 'StAcK OvErFloW',
            'company'    => 'stackoverflow',
            'email'      => '[email protected]',
            'phone'      => '777-777-777-777',
            'address_1'  => '31 Main Street',
            'address_2'  => '', 
            'city'       => 'Chennai',
            'state'      => 'TN',
            'postcode'   => '12345',
            'country'    => 'IN'
        );

        $order = wc_create_order();
        $order->add_product( get_product( '12' ), 2 ); //(get_product with id and next is for quantity)
        $order->set_address( $address, 'billing' );
        $order->set_address( $address, 'shipping' );
        $order->add_coupon('Fresher','10','2'); // accepted param $couponcode, $couponamount,$coupon_tax
        $order->calculate_totals();

Call this above code with your function then it will work accordingly.

Note it not work with old version of WooCommerce like 2.1.12, It works only from 2.2 of WooCommerce.

Hope it helps


2017-2021 For WooCommerce 3 and Above

Woocommerce 3 has introduced CRUD objects and there is lot of changes on Order items… Also some WC_Order methods are now deprecated like add_coupon().

Here is a function that allow creating programmatically an order nicely with all required data in it, including the taxes:

function create_wc_order( $data ){
    $gateways = WC()->payment_gateways->get_available_payment_gateways();
    $order    = new WC_Order();

    // Set Billing and Shipping adresses
    foreach( array('billing_', 'shipping_') as $type ) {
        foreach ( $data['address'] as $key => $value ) {
            if( $type === 'shipping_' && in_array( $key, array( 'email', 'phone' ) ) )
                continue;

            $type_key = $type.$key;

            if ( is_callable( array( $order, "set_{$type_key}" ) ) ) {
                $order->{"set_{$type_key}"}( $value );
            }
        }
    }

    // Set other details
    $order->set_created_via( 'programatically' );
    $order->set_customer_id( $data['user_id'] );
    $order->set_currency( get_woocommerce_currency() );
    $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
    $order->set_customer_note( isset( $data['order_comments'] ) ? $data['order_comments'] : '' );
    $order->set_payment_method( isset( $gateways[ $data['payment_method'] ] ) ? $gateways[ $data['payment_method'] ] : $data['payment_method'] );

    $calculate_taxes_for = array(
        'country'  => $data['address']['country'],
        'state'    => $data['address']['state'],
        'postcode' => $data['address']['postcode'],
        'city'     => $data['address']['city']
    );

    // Line items
    foreach( $data['line_items'] as $line_item ) {
        $args = $line_item['args'];
        $product = wc_get_product( isset($args['variation_id']) && $args['variation_id'] > 0 ? $$args['variation_id'] : $args['product_id'] );
        $item_id = $order->add_product( $product, $line_item['quantity'], $line_item['args'] );

        $item    = $order->get_item( $item_id, false );

        $item->calculate_taxes($calculate_taxes_for);
        $item->save();
    }

    // Coupon items
    if( isset($data['coupon_items'])){
        foreach( $data['coupon_items'] as $coupon_item ) {
            $order->apply_coupon(sanitize_title($coupon_item['code']));
        }
    }

    // Fee items
    if( isset($data['fee_items'])){
        foreach( $data['fee_items'] as $fee_item ) {
            $item = new WC_Order_Item_Fee();

            $item->set_name( $fee_item['name'] );
            $item->set_total( $fee_item['total'] );
            $tax_class = isset($fee_item['tax_class']) && $fee_item['tax_class'] != 0 ? $fee_item['tax_class'] : 0;
            $item->set_tax_class( $tax_class ); // O if not taxable

            $item->calculate_taxes($calculate_taxes_for);

            $item->save();
            $order->add_item( $item );
        }
    }

    // Set calculated totals
    $order->calculate_totals();
        
    if( isset($data['order_status']) ) {
        // Update order status from pending to your defined status and save data
        $order->update_status($data['order_status']['status'], $data['order_status']['note']);
        $order_id = $order->get_id();
    } else {
        // Save order to database (returns the order ID)
        $order_id = $order->save();
    }
    
    // Returns the order ID
    return $order_id;
}

Code goes in function.php file of your active child theme (or active theme) or in a plugin file.


USAGE EXAMPLE from a data array:

create_wc_order( array(
    'address' => array(
        'first_name' => 'Fresher',
        'last_name'  => 'StAcK OvErFloW',
        'company'    => 'stackoverflow',
        'email'      => '[email protected]',
        'phone'      => '777-777-777-777',
        'address_1'  => '31 Main Street',
        'address_2'  => '',
        'city'       => 'Chennai',
        'state'      => 'TN',
        'postcode'   => '12345',
        'country'    => 'IN',
    ),
    'user_id'        => '',
    'order_comments' => '',
    'payment_method' => 'bacs',
    'order_status'   => array(
        'status' => 'on-hold',
        'note'   => '',
    ),
    'line_items' => array(
        array(
            'quantity' => 1,
            'args'     => array(
                'product_id'    => 37,
                'variation_id'  => '',
                'variation'     => array(),
            )
        ),
    ),
    'coupon_items' => array(
        array(
            'code'         => 'summer',
        ),
    ),
    'fee_items' => array(
        array(
            'name'      => 'Delivery',
            'total'     => 5,
            'tax_class' => 0, // Not taxable
        ),
    ),
) );

With the new release of WC 2, it's much better.

However:

  • I do not want to use the REST API, cause I am doing a call from my own WP plugin directly. I see no use in doing a curl to my localhost
  • The 'WooCommerce REST API Client Library' is not useful for me cause it relay's on the REST API and it doesn't support a Create Order call

To be honest, WooCom's API Docs are limited, maybe they are still in the progress of updating it. They currently don't tell me how to create a new order, which params are required etc.

Any way, I figured out how to create an order with a line order (your product) using the classes and functions used by the REST API and I want to share it!

I created my own PHP class:

class WP_MyPlugin_woocommerce
{

public static function init()
{
    // required classes to create an order
    require_once WOOCOMMERCE_API_DIR . 'class-wc-api-exception.php';
    require_once WOOCOMMERCE_API_DIR . 'class-wc-api-server.php';
    require_once WOOCOMMERCE_API_DIR . 'class-wc-api-resource.php';
    require_once WOOCOMMERCE_API_DIR . 'interface-wc-api-handler.php';
    require_once WOOCOMMERCE_API_DIR . 'class-wc-api-json-handler.php';
    require_once WOOCOMMERCE_API_DIR . 'class-wc-api-orders.php';
}

public static function create_order()
{
    global $wp;

    // create order
    $server = new WC_API_Server( $wp->query_vars['wc-api-route'] );
    $order = new WC_API_Orders( $server );

    $order_id = $order->create_order( array
    (
        'order'             => array
        (
           'status'            => 'processing'
        ,  'customer_id'       =>  get_current_user_id()
        // ,   'order_meta'        => array
        //     (
        //        'some order meta'         => 'a value
        //     ,   some more order meta'    => 1
        //     )
        ,   'shipping_address'        => array
            (
                'first_name'          => $firstname
            ,   'last_name'           => $lastname
            ,   'address_1'           => $address
            ,   'address_2'           => $address2
            ,   'city'                => $city
            ,   'postcode'            => $postcode
            ,   'state'               => $state
            ,   'country'             => $country
            )

        ,   'billing_address'        => array(..can be same as shipping )

        ,   'line_items'        => array
            (
                array
                (
                    'product_id'         => 258
                ,   'quantity'           => 1
                )
            )
        )
    ) );
    var_dump($order_id);
    die();
}
}

Important:

  • The 'WOOCOMMERCE_API_DIR' constant points to '/woocommerce/includes/api/' in your plugin dir.
  • The order is assigned to a customer, in my case the current logged in user. Make sure your user has a role that has the capability to read, edit, create and delete orders. My role looks like this:

       $result = add_role(
        'customer'
    ,   __( 'Customer' )
    ,   array
        (
            'read'         => true
        // ,   'read_private_posts' => true
        // ,   'read_private_products' => true
        ,   'read_private_shop_orders' => true
        ,   'edit_private_shop_orders' => true
        ,   'delete_private_shop_orders' => true
        ,   'publish_shop_orders' => true
        // ,   'read_private_shop_coupons' => true
        ,   'edit_posts'   => false
        ,   'delete_posts' => false
        ,   'show_admin_bar_front' => false
        )
    );
    
  • If you want to look at the shop manager's rights, check

    var_dump(get_option( 'wp_user_roles'));

My create_order function nicely creates an order, with the lineitem in the order_items tables.

Hope I helped you out, it took me a while to get it right.