WooCommerce: Duplicate Order @ WordPress Dashboard

In WooCommerce, the ability to quickly duplicate orders can save time when managing repeat purchases or creating similar orders. However, this feature isn’t available by default. While plugins exist to add it, they can be complex or overengineered for simple use cases.

This custom PHP snippet adds a “Duplicate” button to the order actions in the WordPress dashboard. It duplicates the order directly in the database, offering a lightweight solution without unnecessary overhead.

If you need a practical and efficient way to duplicate WooCommerce orders directly from the “Orders” admin page, this approach offers a tailored solution that integrates seamlessly with your existing workflow.

Here are the duplicate buttons you’ll see once the snippet below is active on your website.

PHP Snippet: Add “Duplicate Order” Button @ WooCommerce Orders Table Action

Notes:

  • This snippet will set the cloned order status to “pending”.
  • This won’t reduce the stock on the duplicate order, if the original order reduced stock. This is because the original order has the ‘_order_stock_reduced‘ post meta set to “yes”. The good thing, though, is that you can skip the duplication of this specific post meta by using the ‘bbloomer_excluded_order_meta_keys‘ filter which is provided below, and therefore expect a stock reduction whenever you set the order status to one of the stock reducing statuses.
  • This won’t assign downloadable permissions in case the duplicate order has downloadable products and you set the order status to completed, for the exact reasons as ‘_order_stock_reduced‘ post meta. In this case, you need to exclude ‘download_permissions_granted‘ via the ‘bbloomer_excluded_order_meta_keys‘ filter, so you can grant them when you complete the order
  • Probably, the same problem happens with ‘new_order_email_sent‘, ‘recorded_sales‘, ‘recorded_coupon_usage_countspost meta
  • I don’t use WooCommerce Analytics so not sure if the duplicated order is synced once you complete it

Duplicate Order Snippet #1 – No HPOS

/**
 * @snippet       Duplicate WooCommerce Order (No HPOS)
 * @how-to        businessbloomer.com/woocommerce-customization
 * @author        Rodolfo Melogli, Business Bloomer
 * @compatible    WooCommerce 9
 * @community     https://businessbloomer.com/club/
 */

add_filter( 'woocommerce_admin_order_actions', 'bbloomer_duplicate_order_button', 9999, 2 );

function bbloomer_duplicate_order_button( $actions, $order ) {
	$actions['duplicate_order'] = array(
		'url' => wp_nonce_url( admin_url( 'edit.php?post_type=shop_order&action=duplicate_order&order_id=' . $order->get_id() ), 'bbloomer-duplicate-order' ),
		'name' => __( 'Duplicate', 'woocommerce' ),
		'action' => 'duplicate_order',
	);
	return $actions;
}

add_action( 'admin_head', 'bbloomer_duplicate_order_button_css' );
			
function bbloomer_duplicate_order_button_css() {
	echo '<style>.widefat .column-wc_actions a.wc-action-button-duplicate_order::after { font-family: WooCommerce; content: "\e007" }</style>';
}

add_action( 'admin_action_duplicate_order', 'bbloomer_duplicate_order' );

function bbloomer_duplicate_order() {
	
	if ( current_user_can( 'edit_shop_orders' ) && isset( $_GET['order_id'] ) && check_admin_referer( 'bbloomer-duplicate-order' ) ) {
		
		global $wpdb;
		
		$order_id = absint( wp_unslash( $_GET['order_id'] ) );

		$order = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID = %d", $order_id ), ARRAY_A );
		if ( ! $order ) {
			return new WP_Error( 'invalid_order', __( 'Order not found' ) );
		}

		unset( $order['ID'] );
		$order['post_date'] = current_time( 'mysql' );
		$order['post_date_gmt'] = current_time( 'mysql', 1 );		
    	$order['post_status'] = apply_filters( 'bbloomer_duplicate_order_status', 'wc-pending', $order_id );
    
		$wpdb->insert( $wpdb->posts, $order );
		$new_order_id = $wpdb->insert_id;

		$meta = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d", $order_id ) );
		$excluded_meta_keys = apply_filters( 'bbloomer_excluded_order_meta_keys', [], $order_id );
		foreach ( $meta as $meta_row ) {
			if ( in_array( $meta_row->meta_key, $excluded_meta_keys, true ) ) {
				continue;
			}
			$wpdb->insert( $wpdb->postmeta, [
				'post_id' => $new_order_id,
				'meta_key' => $meta_row->meta_key,
				'meta_value' => $meta_row->meta_value
			] );
		}

		$items = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d", $order_id ), ARRAY_A );
		foreach ( $items as $item ) {
			$old_item_id = $item['order_item_id'];
			unset( $item['order_item_id'] );
			$item['order_id'] = $new_order_id;
			$wpdb->insert( "{$wpdb->prefix}woocommerce_order_items", $item );
			$new_item_id = $wpdb->insert_id;
			$item_meta = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id = %d", $old_item_id ) );
			foreach ( $item_meta as $meta ) {
				$wpdb->insert( "{$wpdb->prefix}woocommerce_order_itemmeta", [
					'order_item_id' => $new_item_id,
					'meta_key' => $meta->meta_key,
					'meta_value' => $meta->meta_value
				] );
			}
		}
		
		wp_safe_redirect( admin_url( 'edit.php?post_type=shop_order' ) );
    	exit;
		
	}
	
}

Duplicate Order Snippet #2 – HPOS

Note: this is untested as I don’t use HPOS.

/**
 * @snippet       Duplicate WooCommerce Order (HPOS)
 * @how-to        businessbloomer.com/woocommerce-customization
 * @author        Rodolfo Melogli, Business Bloomer
 * @compatible    WooCommerce 9
 * @community     https://businessbloomer.com/club/
 */

add_filter( 'woocommerce_admin_order_actions', 'bbloomer_duplicate_order_button', 9999, 2 );

function bbloomer_duplicate_order_button( $actions, $order ) {
	$actions['duplicate_order'] = array(
		'url' => wp_nonce_url( admin_url( 'edit.php?post_type=shop_order&action=duplicate_order&order_id=' . $order->get_id() ), 'bbloomer-duplicate-order' ),
		'name' => __( 'Duplicate', 'woocommerce' ),
		'action' => 'duplicate_order',
	);
	return $actions;
}

add_action( 'admin_head', 'bbloomer_duplicate_order_button_css' );
			
function bbloomer_duplicate_order_button_css() {
	echo '<style>.widefat .column-wc_actions a.wc-action-button-duplicate_order::after { font-family: WooCommerce; content: "\e007" }</style>';
}

add_action( 'admin_action_duplicate_order', 'bbloomer_duplicate_order' );

function bbloomer_duplicate_order() {
	
	if ( current_user_can( 'edit_shop_orders' ) && isset( $_GET['order_id'] ) && check_admin_referer( 'bbloomer-duplicate-order' ) ) {
		
		global $wpdb;
		
		$order_id = absint( wp_unslash( $_GET['order_id'] ) );

		$order = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}wc_orders WHERE id = %d", $order_id ) );
		if ( ! $order ) {
			return new WP_Error( 'invalid_order', __( 'Order not found' ) );
		}

		unset( $order['ID'] );
		$order['post_date'] = current_time( 'mysql' );
		$order['post_date_gmt'] = current_time( 'mysql', 1 );		
    	$order['post_status'] = apply_filters( 'bbloomer_duplicate_order_status', 'wc-pending', $order_id );
    
		$wpdb->insert( "{$wpdb->prefix}wc_orders", $order );
		$new_order_id = $wpdb->insert_id;

		$meta = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM {$wpdb->prefix}wc_order_metadata WHERE post_id = %d", $order_id ) );
		$excluded_meta_keys = apply_filters( 'bbloomer_excluded_order_meta_keys', [], $order_id );
		foreach ( $meta as $meta_row ) {
			if ( in_array( $meta_row->meta_key, $excluded_meta_keys, true ) ) {
				continue;
			}
			$wpdb->insert( "{$wpdb->prefix}wc_order_metadata", [
				'post_id' => $new_order_id,
				'meta_key' => $meta_row->meta_key,
				'meta_value' => $meta_row->meta_value
			] );
		}

		$items = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}wc_order_items WHERE order_id = %d", $order_id ) );
		foreach ( $items as $item ) {
			$old_item_id = $item['order_item_id'];
			unset( $item['order_item_id'] );
			$item['order_id'] = $new_order_id;
			$wpdb->insert( "{$wpdb->prefix}wc_order_items", $item );
			$new_item_id = $wpdb->insert_id;
			$item_meta = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM {$wpdb->prefix}wc_order_itemmeta WHERE order_item_id = %d", $old_item_id ) );
			foreach ( $item_meta as $meta ) {
				$wpdb->insert( "{$wpdb->prefix}wc_order_itemmeta", [
					'order_item_id' => $new_item_id,
					'meta_key' => $meta->meta_key,
					'meta_value' => $meta->meta_value
				] );
			}
		}
		
		wp_safe_redirect( admin_url( 'edit.php?post_type=shop_order' ) );
    	exit;
		
	}
	
}

Where to add custom code?

You should place custom PHP in functions.php and custom CSS in style.css of your child theme: where to place WooCommerce customization?

This code still works, unless you report otherwise. To exclude conflicts, temporarily switch to the Storefront theme, disable all plugins except WooCommerce, and test the snippet again: WooCommerce troubleshooting 101

Related content

Rodolfo Melogli

Business Bloomer Founder

Author, WooCommerce expert and WordCamp speaker, Rodolfo has worked as an independent WooCommerce freelancer since 2011. His goal is to help entrepreneurs and developers overcome their WooCommerce nightmares. Rodolfo loves travelling, chasing tennis & soccer balls and, of course, wood fired oven pizza. Follow @rmelogli

Questions? Feedback? Customization? Leave your comment now!
_____

If you are writing code, please wrap it like so: [php]code_here[/php]. Failure to complying with this, as well as going off topic or not using the English language will result in comment disapproval. You should expect a reply in about 2 weeks - this is a popular blog but I need to get paid work done first. Please consider joining the Business Bloomer Club to get quick WooCommerce support. Thank you!

Your email address will not be published. Required fields are marked *