Managing WooCommerce orders might be a complicated time after time.
There are a lot of solutions to clone WooCommerce orders completely, but not about duplicating individual order items. This is possible to add products into the order individually, but that feature is quite limited.
Fortunately, I have a fast and small solution for you. The lower PHP code adds the "Duplicate" button under the product data.
Here how the button looks like:
What the trick is? Firstly, we need to output a button. Pity there is no hook available to output that button near the "edit" and "remove" buttons, so we only can use available hooks. Better than nothing anyway!
Next, we add a script to handle this button click. It's applied only on the WooCommerce order pages. Pity again if WooCommerce JavaScript API was opened from the outside, that would be possible to write less code. But it's hidden under the closure and have no public handle. Anyway again!
And then we can use WooCommerce PHP public API (yey!) to handle the "clone" AJAX-request. We just use the product add action, and then extend the new added product data by the cloned source data. The script clones all source meta into the new one, even hidden.
The only thing to keep in mind is you need to recalculate order totals to make sure all prices and taxes are fine. Fortunately, it's made via one click.
And finally, the code:
// output clone button
add_action('woocommerce_after_order_itemmeta', function ($itemId, \WC_Order_Item $item, $product) {
$order = $item->get_order();
if (!$order || !$order->is_editable() || !$product instanceof \WC_Product) {
return;
}
echo '<a href="#" data-component="wc-order-item-cloner"' . ' data-id="' . esc_attr($product->get_id()) . '" '
. 'data-qty="' . esc_attr($item->get_quantity()) . '" data-item-id="' . esc_attr($itemId) . '">'
. esc_html__('Duplicate', 'woocommerce')
. '</a>';
}, 10, 3);
// add inline scripts
add_action('admin_print_scripts', function () {
if (empty($_GET['page']) || $_GET['page'] != 'wc-orders') {
return;
}
?>
<script>
(function ($) {
'use strict';
$(document).on('click', '[data-component~="wc-order-item-cloner"]', function (event) {
event.preventDefault();
this.style.pointerEvents = 'none';
return $.ajax({
type: 'POST',
url: woocommerce_admin_meta_boxes.ajax_url,
data: {
action: 'woocommerce_clone_order_item',
order_id: woocommerce_admin_meta_boxes.post_id,
security: woocommerce_admin_meta_boxes.order_item_nonce,
data: [{
id: this.getAttribute('data-id'),
qty: this.getAttribute('data-qty'),
item_id: this.getAttribute('data-item-id')
}]
},
success: function (response) {
if (response.success) {
$('#woocommerce-order-items').find('.inside').empty().append(response.data.html);
// Update notes.
if (response.data.notes_html) {
$('ul.order_notes').empty().append($(response.data.notes_html).find('li'));
}
} else {
window.alert(response.data.error);
}
},
complete: function () {
window.wcTracks.recordEvent('order_edit_add_products', {
order_id: woocommerce_admin_meta_boxes.post_id,
status: $('#order_status').val()
});
},
always: () => this.style.pointerEvents = '',
dataType: 'json'
});
});
})(jQuery);
</script>
<?php
}, 100);
// handle clone ajax action
add_action('wp_ajax_woocommerce_clone_order_item', function () {
try {
\WC_AJAX::add_order_item();
} catch (\Exception $exception) {
wp_send_json_error(['error' => $exception->getMessage()]);
}
});
// clone source product data into the new item
add_filter('woocommerce_ajax_order_item', function (\WC_Order_Item_Product $targetItem, $itemId, \WC_Order $order) {
if (!did_action('wp_ajax_woocommerce_clone_order_item') || empty($_POST['data'])) {
return $targetItem;
}
// find source item
$data = (array) $_POST['data'];
$data = reset($data);
if (empty($data['item_id'])) {
return $targetItem;
}
$sourceItem = $order->get_item($data['item_id']);
if (!$sourceItem instanceof \WC_Order_Item_Product) {
return $targetItem;
}
// clone meta
foreach (get_metadata('order_item', $sourceItem->get_id()) as $key => $value) {
foreach ($value as $valueItem) {
$targetItem->add_meta_data($key, $valueItem, count($value) == 1);
}
}
return $targetItem;
}, 10, 3);
I hope you'll also find it useful. At least I do 🙂 Happy coding!