How to Duplicate an Order in WooCommerce in One Click

In this tutorial, I will discuss how to duplicate any WooCommerce order either programmatically or with the help of a WordPress plugin.

No matter which way you choose, it is going to be a one-click approach; first, we will add a “Duplicate” button to orders’ quick actions:

WooCommerce duplicate order example

If it is not enough, we will continue and add a custom order action (which is available from the edit order page, by the way). Here it is:

WooCommerce duplicate an order using a custom action

Once again, if you came here looking for the plugin approach and don’t want to deal with the code, please skip to the appropriate part of this tutorial.

Duplicating an Order Programmatically

Ok, let’s start with the code approach. I am going to show you shortly how to add a quick action button on the WooCommerce > Orders page and a custom order action on the edit order page. Technically, both of these methods allow duplicate orders in WooCommerce in one click. Well, the second method – in two clicks.

Add a custom button to order quick actions

Here is what it is going to look like:

Add a custom button to order quick actions which allows duplicate orders.

In order to manipulate the buttons in order quick actions (to add, to rename, or to resort), we can easily use the woocommerce_admin_order_actions filter hook.

add_filter( 'woocommerce_admin_order_actions', 'rudr_order_actions_button', 25, 2 );

function rudr_order_actions_button( $actions, $order ) {

	$actions[ 'duplicate_order' ] = array(
		'url' => wp_nonce_url(
			add_query_arg(
				array(
					'action' => 'rudr_duplicate',
					'order_id' => $order->get_id(),
				),
				'admin-post.php',
			),
			'rudr_duplicate_order_' . $order->get_id()
		),
		'name' => 'Duplicate order',
		'action' => 'duplicate'
	);

	return $actions;

}

Basically, what I’m doing in the code above is adding one new element to the $actions array. This new array element contains the following parameters:

  • url – here I am using admin-post.php to process the order duplication. I wrapped everything into the wp_nonce_url() function as well.
  • name – it is going to be displayed when you hover over the button.
  • action – CSS class of the button.

In order to process POST requests sent to the admin-post.php file we need to use admin_post_{$action} action hook.

Here is how:

add_action( 'admin_post_rudr_duplicate', function() {

	$order_id = ! empty( $_GET[ 'order_id' ] ) ? absint( $_GET[ 'order_id' ] ) : 0;

	check_admin_referer( "rudr_duplicate_order_{$order_id}" );

	rudr_duplicate_order( $order_id );

	wp_safe_redirect(
		add_query_arg(
			array(
				'page' => 'wc-orders',
				'bulk_action' => 'duplicated',
				'changed' => 1,
			),
			admin_url(  'admin.php' )
		)
	);
	exit;

} );

Please take a look at the rudr_duplicate_order() function; it is not declared yet. We’re going to talk about it later in this tutorial.

Everything else should be clear for you:

  • We’re getting an order ID from the $_GET array,
  • Then we check the nonce with the check_admin_referer() function,
  • And then redirect to the “All orders” page in order to display a success notice.

By the way, the quick action buttons use the Dashicons icon set by default, but you can also use the WooCommerce set if you want.

Here is how to display a “Duplicate” icon from the Dashicons set.

.widefat .column-wc_actions a.duplicate:after {
	content: "\f105"
}

Last but not least, admin notices:

add_action( 'admin_notices', function() {

	$screen = get_current_screen();

	// not an order page
	if( 'woocommerce_page_wc-orders' !== $screen->id && 'edit-shop_order' !== $screen->id ) {
		return;
	}

	$changed = isset( $_GET[ 'changed' ] ) && is_numeric( $_GET[ 'changed' ] ) ? $_GET[ 'changed' ] : 1;
	
	if( isset( $_GET[ 'bulk_action' ] ) && 'duplicated' === $_GET[ 'bulk_action' ] ) {
		echo '<div class="notice notice-success"><p>' . sprintf( _n( '%d order duplicated.', '%d orders duplicated.', $changed ), $changed ) . '</p></div>';
	}

} );

Okay, I admit, I probably overcomplicate things here a little bit. But I thought that it would be great to make these notices work even if later we decide to add more functionality and, for example, duplicate orders using the bulk actions.

Also, please take a look that I am using two different screen IDs in the condition here:

  • woocommerce_page_wc-orders – for HPOS orders,
  • edit-shop_order – for CPT-based orders.

Add a custom order action

That’s definitely an optional step because we already added the “Duplicate order” button in the previous step.

However, let’s take a look and add it anyway.

create a custom order action which allows to duplicate orders
The only downside – to use this feature, you would need to go to the edit order page.

Below is the code that allows you to add this custom order action:

// add a custom order action
add_action( 'woocommerce_order_actions', 'rudr_custom_order_action' );

function rudr_custom_order_action( $actions ) {
	$actions[ 'duplicate_order' ] = 'Duplicate order';
	return $actions;
}

// connect a function which actually duplicates the order
// add_action( 'woocommerce_order_action_{ACTION SLUG}', ...
add_action( 'woocommerce_order_action_duplicate_order', function( $order ) {
	
	$new_order_id = rudr_duplicate_order( $order->get_id() );
	$new_order = wc_get_order( $new_order_id );
	// redirect to the edit order page of an order duplicate
	wp_safe_redirect( $new_order->get_edit_order_url() );
	exit;

} );

It is actually very nice that we can use the get_edit_order_url() method of the WC_Order class because it actually allows us to get the correct edit order URL, no matter whether HPOS is in use or not.

The callback function, which actually duplicates an order

It is going to be interesting, guys! When I investigated different approaches and tutorials, I was freaked out that dudes were using update_post_meta() function when duplicating, let’s say, order billing and shipping addresses.

How do you think – will this method work when high-performance order storage is in use?

Not a chance!

/*
 * WooCommerce duplicate order function
 * 
 * @author: Misha Rudrastyh
 * @url: https://rudrastyh.com/woocommerce/duplicate-order.html
 */
function rudr_duplicate_order( $order_id ) {

	$order = wc_get_order( $order_id );
	if( ! $order ) {
		return false;
	}

	$order_data = $order->get_data();

	$new_order = new WC_Order();

	$new_order->set_currency( $order_data[ 'currency' ] );
	$new_order->set_prices_include_tax( $order_data[ 'prices_include_tax' ] );

	// status
	$new_order->set_status( 'pending' ); // or we can use $order_data[ 'status' ]
	$new_order->set_created_via( $order_data[ 'created_via' ] ); // admin, checkout, store-api
	// dates
	$new_order->set_date_created( current_time( 'mysql' ) );
	$new_order->set_date_paid( $order_data[ 'date_paid' ] );
	$new_order->set_date_completed( $order_data[ 'date_completed' ] );
	$new_order->set_date_modified( $order_data[ 'date_modified' ] );

	// customer
	$new_order->set_customer_id( $order_data[ 'customer_id' ] );
	$new_order->set_customer_ip_address( $order_data[ 'customer_ip_address' ] );
	$new_order->set_customer_user_agent( $order_data[ 'customer_user_agent' ] );
	//$order->set_customer_note( $order_data[ 'customer_note' ] );

	// billing and shipping addresses
	$new_order->set_address( $order_data[ 'billing' ], 'billing' );
	$new_order->set_address( $order_data[ 'shipping' ], 'shipping' );

	// payment methods
	$new_order->set_payment_method( $order_data[ 'payment_method' ] );
	$new_order->set_payment_method_title( $order_data[ 'payment_method_title' ] );
	$new_order->set_transaction_id( $order_data[ 'transaction_id' ] );

	// other meta data
	foreach( $order->get_meta_data() as $meta ) {
		$new_order->add_meta_data( $meta->key, $meta->value, true );
	}

	// order items – products
	foreach( $order->get_items( 'line_item' ) as $line_item ) {
		$product = $line_item->get_product();
		if( $product ) {
			$new_line_item = new WC_Order_Item_Product();
			$new_line_item->set_product_id( $line_item->get_product_id() );
			$new_line_item->set_variation_id( $line_item->get_variation_id() );
			$new_line_item->set_quantity( $line_item->get_quantity() );
			$new_line_item->set_subtotal( (string) $line_item->get_subtotal() );
			$new_line_item->set_total( (string) $line_item->get_total() );
			foreach( $line_item->get_meta_data() as $meta ) {
				$new_line_item->add_meta_data( $meta->key, $meta->value, true );
			}

			$new_order->add_item( $new_line_item );
		}
	}

	// order items – shipping
	foreach( $order->get_items( 'shipping' ) as $shipping_line ) {
		$new_shipping_line = new WC_Order_Item_Shipping();
		$new_shipping_line->set_method_title( $shipping_line->get_method_title() );
		$new_shipping_line->set_method_id( $shipping_line->get_method_id() );
		$new_shipping_line->set_total( $shipping_line->get_total() );
		$new_shipping_line->set_taxes( $shipping_line->get_taxes() );

		$new_order->add_item( $new_shipping_line );
	}

	// order items – fees
	foreach( $order->get_items( 'fee' ) as $fee_line ) {
		$fee = new WC_Order_Item_Fee();
		$fee->set_name( $fee_line->get_name() );
		$fee->set_amount( $fee_line->get_amount() );
		$fee->set_total( $fee_line->get_total() );

		$new_order->add_item( $fee );
	}
	
	$new_order->calculate_totals();
	$new_order->save();

	// applying coupons
	foreach( $order->get_items( 'coupon' ) as $coupon_item ) {
		$new_order->apply_coupon( $coupon_item->get_code() );
	}

	// we can add order notes only after an order has been created
	$new_order->add_order_note( 
		sprintf( 'This order was duplicated from order #%d.', $order->get_id() ) 
	);

	return $new_order->get_id();

}

There are a couple of things I’d like to highlight in this function:

  • On line 22, I hardcoded the order status of a duplicated order to pending. Sure thing, it is not necessary; you can preserve the original order status from $order_data[ 'status' ].
  • I consider it would be better to create a new order using new WC_Order() instead of the wc_create_order() function.
  • Probably, it would be great to exclude some system custom fields when duplicating an order in your WooCommerce store, for example _edit_lock, on line 47.
  • Though WooCommerce itself doesn’t use custom fields for order items, it doesn’t mean that you don’t have a plugin installed, which does. That’s why, on lines 60-62, I don’t forget about copying custom fields for order items as well.
  • Preserving coupons added to an order is a tricky thing. If you would like to apply it as an order item, then you will need to calculate the discount for each order item individually. I think you will agree that it will be much better to use the apply_coupon() method instead.

Using WordPress Plugins for Duplicating Orders

The good news, guys, is that absolutely the same functionality (and even more) is available as a WordPress plugin.

You can get Duplicate Order for WooCommerce from the WordPress plugin repository.

More than that, if you’re interested in duplicating WooCommerce orders from different stores to a single master store, I’d like you to check my premium plugins:

If you have any questions, feel free to ask in the comments section below.

Misha Rudrastyh

Misha Rudrastyh

Hey guys and welcome to my website. For more than 15 years I've been doing my best to share with you some superb WordPress guides and tips for free.

Need some developer help? Contact me

Follow me on X