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:

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:

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:

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 usingadmin-post.phpto process the order duplication. I wrapped everything into thewp_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
$_GETarray, - 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.

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 thewc_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:
- Order Sync for WooCommerce – for standalone installations,
- Multisite Order Sync for WooCommerce – for WordPress multisite networks.
If you have any questions, feel free to ask in the comments section below.
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
Hi Misha,
thank you for providing the function to duplicate orders. It would be nice if it would also retrieve the name of the product. So far it only seems to copy the id, but that looks odd in the order, if I send it to the customer for review and he doesn´t know what the numbers mean. I would also prefer to have the function use the actual prices the items have on the day of duplicating instead of copying forward prices that are long obsolete because the duplicated order maybe is from 2022. Seems more logic for me from a seller´s view.
Unfortunately the function doesn´t check for different VAT rates and so the items get the standard rate even if the item´s setting is reduced rate.
Regards
Michael
Hi Michael,
The prices are copied as they are supposed to be. I understand that in your case it may make more sense to update the prices to actual ones.
However, I am not sure I did understand about product names. Or do you mean the same – just to update them to the actual ones?
Hi Misha,
Thank you for all your sharings, your blog helps a lot!
I think that Michael pointed out that product names are not copied on the new order.
I simply fixed this by adding the following line at line 60 of the main function :
$new_line_item->set_name( $line_item->get_name() );Hi Clément,
Thanks :)