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.
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
// better to use admin_enqueue_scripts, but made it this way to demonstrate easier
add_action('admin_print_scripts', function () {
if (!((!empty($_GET['page']) && $_GET['page'] == 'wc-orders')
|| (!empty($_GET['post']) && get_post_type($_GET['post']) == 'shop_order'))
) {
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: (response) => response.success ? window.location.reload() : 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;
}
$keysToSkip = [
'_line_subtotal',
'_line_subtotal_tax',
'_line_tax',
'_line_tax_data',
'_line_total',
'_qty',
];
// clone meta
foreach (get_metadata('order_item', $sourceItem->get_id()) as $key => $value) {
if (in_array($key, $keysToSkip)) {
continue;
}
foreach ($value as $valueItem) {
$valueItem = maybe_unserialize($valueItem);
$targetItem->add_meta_data($key, $valueItem, count($value) == 1);
}
}
// add these meta items via methods cause can be doubled in the DB alternatively
$targetItem->set_subtotal($sourceItem->get_subtotal());
$targetItem->set_subtotal_tax($sourceItem->get_subtotal_tax());
$targetItem->set_total($sourceItem->get_total());
$targetItem->set_total_tax($sourceItem->get_total_tax());
$targetItem->set_taxes($sourceItem->get_taxes());
$targetItem->set_quantity($sourceItem->get_quantity());
$targetItem->save();
return $targetItem;
}, 10, 3);
I hope you'll also find it useful. At least I do 🙂 Happy coding!