Commit 8194055b339 for woocommerce

commit 8194055b3397c7e8c3da0b1cdf139b1d5151fecf
Author: Ann <annchichi@users.noreply.github.com>
Date:   Wed Jul 22 11:36:58 2026 +0800

    Fix shipping method names on manual orders (#66811)

    * Fix manual order shipping method names

    * Add changelog entry for manual order shipping name fix

    * Fix shipping title when clearing method

diff --git a/plugins/woocommerce/changelog/fix-manual-order-shipping-method-name b/plugins/woocommerce/changelog/fix-manual-order-shipping-method-name
new file mode 100644
index 00000000000..961a2c9067c
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-manual-order-shipping-method-name
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Use the selected shipping method name for manually created orders.
diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js
index 187e7163230..d2d7ee740f1 100644
--- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js
+++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js
@@ -1,5 +1,35 @@
 // eslint-disable-next-line max-len
-/*global woocommerce_admin_meta_boxes, woocommerce_admin, accounting, woocommerce_admin_meta_boxes_order, wcSetClipboard, wcClearClipboard, wc_enhanced_select_params */
+/*global woocommerce_admin_meta_boxes, woocommerce_admin, accounting, woocommerce_admin_meta_boxes_order, wcSetClipboard, wcClearClipboard, wc_enhanced_select_params, module */
+
+/**
+ * Get the shipping method title to use after a method selection changes.
+ *
+ * @param {Object} args               Shipping title data.
+ * @param {string} args.currentTitle  Current shipping title.
+ * @param {string} args.defaultTitle  Default shipping title.
+ * @param {string} args.previousTitle Previously selected method title.
+ * @param {string} args.methodValue   Selected shipping method value.
+ * @param {string} args.methodTitle   Selected shipping method title.
+ * @return {string} The shipping title to use.
+ */
+function getShippingMethodTitle( args ) {
+	if (
+		args.currentTitle
+		&& args.currentTitle !== args.defaultTitle
+		&& args.currentTitle !== args.previousTitle
+	) {
+		return args.currentTitle;
+	}
+
+	return args.methodValue && 'other' !== args.methodValue
+		? args.methodTitle
+		: args.defaultTitle;
+}
+
+if ( typeof module !== 'undefined' && module.exports ) {
+	module.exports = { getShippingMethodTitle };
+}
+
 jQuery( function ( $ ) {

 	// Stand-in wcTracks.recordEvent in case tracks is not available (for any reason).
@@ -283,6 +313,7 @@ jQuery( function ( $ ) {
 				.on( 'click', 'button.calculate-action', this.recalculate )
 				.on( 'click', 'a.edit-order-item', this.edit_item )
 				.on( 'click', 'a.delete-order-item', this.delete_item )
+				.on( 'change', 'select.shipping_method', this.shipping_method_changed )

 				// Refunds
 				.on( 'click', '.delete_refund', this.refunds.delete_refund )
@@ -376,6 +407,30 @@ jQuery( function ( $ ) {
 			wc_meta_boxes_order_items.stupidtable.init();
 		},

+		shipping_method_changed: function() {
+			var $select       = $( this );
+			var $name         = $select.closest( 'tr.shipping' ).find( 'input.shipping_method_name' );
+			var title         = $select.find( 'option:selected' ).text();
+			var previousTitle = $select.data( 'selected-title' ) || $select.find( 'option' ).filter( function() {
+				return this.defaultSelected;
+			} ).text();
+			var currentTitle  = $name.val();
+			var defaultTitle  = $name.data( 'default-shipping-title' );
+			var nextTitle     = getShippingMethodTitle( {
+				currentTitle: currentTitle,
+				defaultTitle: defaultTitle,
+				previousTitle: previousTitle,
+				methodValue: $select.val(),
+				methodTitle: title
+			} );
+
+			$select.data( 'selected-title', title );
+
+			if ( currentTitle !== nextTitle ) {
+				$name.val( nextTitle ).trigger( 'change' );
+			}
+		},
+
 		// When the qty is changed, increase or decrease costs
 		quantity_changed: function() {
 			var $row          = $( this ).closest( 'tr.item' );
diff --git a/plugins/woocommerce/client/legacy/js/admin/test/meta-boxes-order.test.js b/plugins/woocommerce/client/legacy/js/admin/test/meta-boxes-order.test.js
new file mode 100644
index 00000000000..7bd51d99ef2
--- /dev/null
+++ b/plugins/woocommerce/client/legacy/js/admin/test/meta-boxes-order.test.js
@@ -0,0 +1,42 @@
+/**
+ * Tests for the order meta box shipping method name synchronization.
+ */
+
+global.jQuery = jest.fn();
+
+const { getShippingMethodTitle } = require( '../meta-boxes-order' );
+
+describe( 'Order meta box shipping method names', () => {
+	test( 'uses the selected method title for the default shipping name', () => {
+		expect( getShippingMethodTitle( {
+			currentTitle: 'Shipping',
+			defaultTitle: 'Shipping',
+			previousTitle: 'N/A',
+			methodValue: 'free_shipping',
+			methodTitle: 'Free shipping',
+		} ) ).toBe( 'Free shipping' );
+	} );
+
+	test.each( [
+		[ 'N/A', '', 'N/A' ],
+		[ 'Other', 'other', 'Other' ],
+	] )( 'resets an auto-synced name when selecting %s', ( label, methodValue, methodTitle ) => {
+		expect( getShippingMethodTitle( {
+			currentTitle: 'Free shipping',
+			defaultTitle: 'Shipping',
+			previousTitle: 'Free shipping',
+			methodValue,
+			methodTitle,
+		} ) ).toBe( 'Shipping' );
+	} );
+
+	test( 'preserves a custom name when selecting N/A', () => {
+		expect( getShippingMethodTitle( {
+			currentTitle: 'Local courier',
+			defaultTitle: 'Shipping',
+			previousTitle: 'Free shipping',
+			methodValue: '',
+			methodTitle: 'N/A',
+		} ) ).toBe( 'Local courier' );
+	} );
+} );
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-shipping.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-shipping.php
index c00470d6eff..96477220845 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-shipping.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-shipping.php
@@ -23,7 +23,14 @@ if ( ! defined( 'ABSPATH' ) ) {
 		</div>
 		<div class="edit" style="display: none;">
 			<input type="hidden" name="shipping_method_id[]" value="<?php echo esc_attr( $item_id ); ?>" />
-			<input type="text" class="shipping_method_name" placeholder="<?php esc_attr_e( 'Shipping name', 'woocommerce' ); ?>" name="shipping_method_title[<?php echo esc_attr( $item_id ); ?>]" value="<?php echo esc_attr( $item->get_name() ); ?>" />
+			<input
+				type="text"
+				class="shipping_method_name"
+				placeholder="<?php esc_attr_e( 'Shipping name', 'woocommerce' ); ?>"
+				name="shipping_method_title[<?php echo esc_attr( $item_id ); ?>]"
+				value="<?php echo esc_attr( $item->get_name() ); ?>"
+				data-default-shipping-title="<?php esc_attr_e( 'Shipping', 'woocommerce' ); ?>"
+			/>
 			<select class="shipping_method" name="shipping_method[<?php echo esc_attr( $item_id ); ?>]">
 				<optgroup label="<?php esc_attr_e( 'Shipping method', 'woocommerce' ); ?>">
 					<option value=""><?php esc_html_e( 'N/A', 'woocommerce' ); ?></option>
diff --git a/plugins/woocommerce/includes/admin/wc-admin-functions.php b/plugins/woocommerce/includes/admin/wc-admin-functions.php
index 3058f360083..03cab7ad3f4 100644
--- a/plugins/woocommerce/includes/admin/wc-admin-functions.php
+++ b/plugins/woocommerce/includes/admin/wc-admin-functions.php
@@ -404,12 +404,13 @@ function wc_save_order_items( $order_id, $items ) {

 	// Shipping Rows.
 	if ( isset( $items['shipping_method_id'] ) ) {
-		$data_keys = array(
+		$data_keys        = array(
 			'shipping_method'       => null,
 			'shipping_method_title' => null,
 			'shipping_cost'         => 0,
 			'shipping_taxes'        => array(),
 		);
+		$shipping_methods = null;

 		foreach ( $items['shipping_method_id'] as $item_id ) {
 			$item = WC_Order_Factory::get_order_item( absint( $item_id ) );
@@ -424,6 +425,22 @@ function wc_save_order_items( $order_id, $items ) {
 				$item_data[ $key ] = isset( $items[ $key ][ $item_id ] ) ? wc_clean( wp_unslash( $items[ $key ][ $item_id ] ) ) : $default;
 			}

+			$item_data['shipping_method']       = is_string( $item_data['shipping_method'] ) ? $item_data['shipping_method'] : '';
+			$item_data['shipping_method_title'] = is_string( $item_data['shipping_method_title'] ) ? $item_data['shipping_method_title'] : '';
+
+			if (
+				! empty( $item_data['shipping_method'] ) &&
+				in_array( $item_data['shipping_method_title'], array( '', __( 'Shipping', 'woocommerce' ) ), true )
+			) {
+				if ( null === $shipping_methods ) {
+					$shipping_methods = WC()->shipping() ? WC()->shipping()->load_shipping_methods() : array();
+				}
+
+				if ( isset( $shipping_methods[ $item_data['shipping_method'] ] ) ) {
+					$item_data['shipping_method_title'] = $shipping_methods[ $item_data['shipping_method'] ]->get_method_title();
+				}
+			}
+
 			$item->set_props(
 				array(
 					'method_id'    => $item_data['shipping_method'],
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-functions-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-functions-test.php
index 6619f308264..ec1727078d7 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-functions-test.php
@@ -581,4 +581,34 @@ class WC_Admin_Functions_Test extends \WC_Unit_Test_Case {
 		$this->assertEquals( $original_product_id, $saved_item->get_product_id(), 'Reserved internal key should not overwrite core item data' );
 		$this->assertFalse( $saved_item->meta_exists( '_reduced_stock' ), 'Reserved hidden key should not be stored as item meta' );
 	}
+
+	/**
+	 * @testdox wc_save_order_items() should use the shipping method title when the posted shipping title is the default label.
+	 *
+	 * @link https://github.com/woocommerce/woocommerce/issues/36049
+	 */
+	public function test_wc_save_order_items_uses_shipping_method_title_when_posted_shipping_title_is_default_label(): void {
+		$order         = WC_Helper_Order::create_order();
+		$shipping_item = new WC_Order_Item_Shipping();
+		$shipping_item->set_order_id( $order->get_id() );
+		$item_id = $shipping_item->save();
+
+		$items = array(
+			'shipping_method_id'    => array( $item_id ),
+			'shipping_method'       => array( $item_id => 'free_shipping' ),
+			'shipping_method_title' => array( $item_id => __( 'Shipping', 'woocommerce' ) ),
+			'shipping_cost'         => array( $item_id => 0 ),
+			'shipping_taxes'        => array( $item_id => array() ),
+		);
+
+		wc_save_order_items( $order->get_id(), $items );
+
+		$saved_item = new WC_Order_Item_Shipping( $item_id );
+
+		$this->assertSame(
+			'Free shipping',
+			$saved_item->get_name(),
+			'Known shipping methods should use their title when the posted title is the default label'
+		);
+	}
 }