Tạo shipping method WooCommerce: tự viết phương thức vận chuyển tính phí theo cân nặng

Chào anh em! Mấy phương thức vận chuyển có sẵn của WooCommerce (đồng giá, miễn phí, nhận tại cửa hàng) đủ cho shop đơn giản. Nhưng cần tính phí theo cân nặng kiểu “2 kg đầu 30.000đ, mỗi kg thêm 5.000đ”, hay gọi API hãng vận chuyển lấy phí thật, thì phải tự viết. Bài này hướng dẫn tạo shipping method WooCommerce từ đầu, xong bài anh em có một phương thức tính phí theo cân nặng mà chủ shop tự đặt giá cho từng khu vực giao hàng.

Tạo shipping method WooCommerce tính phí ship theo cân nặng bằng PHP

Shipping method trong WooCommerce hoạt động thế nào?

Mỗi phương thức vận chuyển là một lớp kế thừa WC_Shipping_Method. Khi giỏ hàng có địa chỉ giao, WooCommerce gom hàng thành “gói” (package) rồi gọi hàm calculate_shipping() của từng phương thức đang bật. Hàm này thêm một hoặc vài mức phí bằng add_rate(), khách chọn một mức ở trang thanh toán.

Có hai kiểu phương thức:

Để duy trì blog nên mình có làm aff cho 1 số bên hosting. Nhưng dù aff mình cũng chọn 1 số nhà cung cấp uy tín về chất lượng và support nên các bạn cứ yên tâm nhé.

Nếu có mua hosting mà có trong list dưới đây các bạn click vào link trước khi mua để ủng hộ mình nhé. Mình cảm ơn nhiều

  • Kiểu theo khu vực giao hàng (khai shipping-zones trong supports) cho chủ shop thêm phương thức vào từng khu vực, mỗi lần thêm là một “instance” có cài đặt riêng. Đồng giá, miễn phí của WooCommerce là kiểu này.
  • Kiểu toàn website (không khai shipping-zones) không cần thêm vào khu vực nào, được tính cho mọi gói hàng và có trang cài đặt riêng ở tab Vận chuyển. Plugin kết nối hãng vận chuyển hay dùng kiểu này, ví dụ plugin Giao Hàng Tiết Kiệm (GHTK) cho WooCommerce của mình.

Ví dụ dưới làm theo khu vực giao hàng, vì linh hoạt hơn cho shop tự đặt phí: nội thành một giá, liên tỉnh một giá, chỉ việc thêm hai instance vào hai khu vực.

Code đầy đủ: phí ship theo cân nặng

Đặt vào một plugin riêng (ví dụ wp-content/plugins/devvn-ship-by-weight/devvn-ship-by-weight.php kèm header plugin) hoặc file mu-plugin. Đừng dán vào functions.php của giao diện, đổi giao diện là mất phương thức vận chuyển.

add_action( 'woocommerce_shipping_init', 'devvn_ship_by_weight_init' );
add_filter( 'woocommerce_shipping_methods', 'devvn_ship_by_weight_register' );

/**
 * Khai báo phương thức với WooCommerce.
 *
 * @param array $methods Danh sách id => tên lớp.
 * @return array
 */
function devvn_ship_by_weight_register( $methods ) {
	$methods['devvn_ship_by_weight'] = 'DevVN_Ship_By_Weight';
	return $methods;
}

/**
 * Nạp lớp phương thức sau khi WooCommerce đã sẵn sàng.
 *
 * @return void
 */
function devvn_ship_by_weight_init() {
	if ( class_exists( 'DevVN_Ship_By_Weight' ) ) {
		return;
	}

	/**
	 * Phí ship theo cân nặng: giá mở đầu cho N kg đầu, cộng thêm theo từng kg, miễn phí từ một mức giá trị đơn.
	 */
	class DevVN_Ship_By_Weight extends WC_Shipping_Method {

		/**
		 * Khởi tạo một instance của phương thức trong khu vực giao hàng.
		 *
		 * @param int $instance_id ID instance do WooCommerce cấp.
		 */
		public function __construct( $instance_id = 0 ) {
			$this->id                 = 'devvn_ship_by_weight';
			$this->instance_id        = absint( $instance_id );
			$this->method_title       = 'Phí ship theo cân nặng';
			$this->method_description = 'Giá mở đầu cho số kg đầu, cộng thêm theo từng kg.';
			$this->supports           = array( 'shipping-zones', 'instance-settings', 'instance-settings-modal' );

			$this->instance_form_fields = array(
				'title'       => array(
					'title'   => 'Tên hiển thị',
					'type'    => 'text',
					'default' => 'Giao hàng tiêu chuẩn',
				),
				'base_cost'   => array(
					'title'   => 'Giá mở đầu (đ)',
					'type'    => 'number',
					'default' => 30000,
				),
				'base_weight' => array(
					'title'   => 'Số kg tính giá mở đầu',
					'type'    => 'number',
					'default' => 2,
				),
				'extra_cost'  => array(
					'title'   => 'Mỗi kg tiếp theo (đ)',
					'type'    => 'number',
					'default' => 5000,
				),
				'free_from'   => array(
					'title'       => 'Miễn phí khi đơn từ (đ)',
					'type'        => 'number',
					'default'     => 0,
					'description' => 'Để 0 nếu không miễn phí.',
				),
			);

			$this->title = $this->get_option( 'title' );
			add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
		}

		/**
		 * Tính phí cho một gói hàng và thêm mức phí cho khách chọn.
		 *
		 * @param array $package Gói hàng: contents, contents_cost, destination.
		 * @return void
		 */
		public function calculate_shipping( $package = array() ) {
			$weight    = $this->get_package_weight( $package );
			$free_from = (float) $this->get_option( 'free_from' );
			if ( $free_from > 0 && $package['contents_cost'] >= $free_from ) {
				$cost = 0;
			} else {
				$extra_kg = max( 0, ceil( $weight - (float) $this->get_option( 'base_weight' ) ) );
				$cost     = (float) $this->get_option( 'base_cost' ) + $extra_kg * (float) $this->get_option( 'extra_cost' );
			}

			$this->add_rate(
				array(
					'id'      => $this->get_rate_id(),
					'label'   => $this->title,
					'cost'    => $cost,
					'package' => $package,
				)
			);
		}

		/**
		 * Tổng khối lượng gói hàng, quy về kg theo đơn vị cân nặng shop đang dùng.
		 *
		 * @param array $package Gói hàng.
		 * @return float
		 */
		protected function get_package_weight( $package ) {
			$weight = 0;
			foreach ( $package['contents'] as $item ) {
				$weight += (float) wc_get_weight( (float) $item['data']->get_weight(), 'kg' ) * $item['quantity'];
			}
			return $weight;
		}
	}
}

Kích hoạt xong, vào WooCommerce → Cài đặt → Vận chuyển, mở một khu vực giao hàng, bấm thêm phương thức sẽ thấy “Phí ship theo cân nặng”. Thêm vào rồi bấm sửa để đặt giá cho khu vực đó.

Giải thích từng phần

  • Phương thức đăng ký qua hai hook: woocommerce_shipping_init nạp lớp khi WooCommerce đã có WC_Shipping_Method, còn filter woocommerce_shipping_methods khai id phương thức với tên lớp. Viết lớp ngoài hook mà WooCommerce chưa nạp là lỗi “class not found”.
  • Trong mảng supports, shipping-zones cho phép thêm vào khu vực, instance-settings cho mỗi instance cài đặt riêng, còn instance-settings-modal mở cài đặt trong hộp thoại ngay tại trang khu vực.
  • instance_form_fields khai ô cài đặt của từng instance, và get_option() đọc đúng giá trị của instance đang tính. Phương thức kiểu toàn website thì dùng form_fields thay vào.
  • WooCommerce cho shop chọn đơn vị cân nặng g hoặc kg, nên luôn quy đổi bằng wc_get_weight() rồi mới cộng. Sản phẩm để trống cân nặng tính là 0, nhớ nhắc chủ shop khai đủ.
  • contents_cost là tổng tiền hàng của gói (chưa tính phí ship), dùng để xét mức miễn phí.
  • get_rate_id() trả về dạng devvn_ship_by_weight:5 (id kèm số instance), nhờ vậy hai instance ở hai khu vực không đè nhau.

Ẩn hiện phương thức theo điều kiện

Muốn phương thức chỉ hiện trong một số trường hợp thì ghi đè is_available() ngay trong lớp, dùng lại hàm get_package_weight() ở trên. Ví dụ không nhận đơn quá 30 kg:

/**
 * Ẩn phương thức khi gói hàng nặng quá mức cho phép.
 *
 * @param array $package Gói hàng.
 * @return bool
 */
public function is_available( $package ) {
	return $this->get_package_weight( $package ) <= 30 && parent::is_available( $package );
}

Nhớ gọi parent::is_available() để giữ các kiểm tra sẵn có của WooCommerce. Với phương thức của người khác (không sửa được lớp), WooCommerce có filter woocommerce_shipping_{id}_is_available. Mình có ví dụ dùng filter này để chỉ bật phí GHTK ở một số tỉnh.

Lấy phí từ API hãng vận chuyển

Khung lớp giữ nguyên, chỉ thay phần tính $cost bằng một lần gọi API. Ba điều cần nhớ:

  • Địa chỉ nằm trong $package['destination'] (country, state, city, address…). Website Việt Nam mà dùng plugin địa chỉ thì các ô này chứa mã tỉnh, mã phường của plugin, phải đổi ra tên hoặc mã mà hãng vận chuyển cần.
  • Nên cache kết quả, vì mỗi lần khách đổi địa chỉ là một lần tính lại. Lưu phí theo bộ tham số bằng transient để khỏi gọi API lặp lại.
  • API lỗi thì đừng làm hỏng trang thanh toán: không có phí thì không gọi add_rate() (phương thức tự ẩn) hoặc thêm một mức “báo phí sau”, tuỳ cách shop muốn xử lý.

Ví dụ gọi API phí ship của GHTK có trong bài tích hợp Giao Hàng Tiết Kiệm vào WordPress; client đầy đủ có xử lý lỗi ở bài GHTK API bằng PHP.

Kiểm tra và lỗi hay gặp

  • Sửa code mà phí không đổi là do WooCommerce lưu phí đã tính trong phiên của khách. Khi đang phát triển, vào WooCommerce → Cài đặt → Vận chuyển, mục tuỳ chọn vận chuyển (tên mục hơi khác tuỳ bản WooCommerce), bật Debug mode để bỏ qua bộ nhớ đệm phí ship. Xong việc nhớ tắt.
  • Không thấy phương thức trong danh sách thêm vào khu vực là do thiếu shipping-zones trong supports, hoặc filter đăng ký chưa chạy (plugin chưa kích hoạt, gõ sai tên lớp).
  • Khách không thấy phương thức ở trang thanh toán khi địa chỉ khách không thuộc khu vực có phương thức này, khi is_available() trả false, hoặc khi calculate_shipping() không gọi add_rate().
  • Phương thức viết theo cách trên vẫn hiện ở giỏ hàng và trang thanh toán dạng block, vì block lấy phí qua chính hệ thống shipping của WooCommerce. Chỗ hay vướng ở trang block là các ô địa chỉ tuỳ biến của plugin địa chỉ.

Câu hỏi thường gặp

Một phương thức trả nhiều mức phí được không?

Được. Gọi add_rate() nhiều lần với id khác nhau, ví dụ $this->get_rate_id( 'nhanh' ) và $this->get_rate_id( 'tiet-kiem' ); khách sẽ thấy hai lựa chọn.

Nên làm kiểu theo khu vực hay toàn website?

Phí do shop tự đặt theo vùng thì theo khu vực, chủ shop dễ quản lý. Phí lấy từ API hãng vận chuyển (đã tính theo địa chỉ thật) thì kiểu toàn website gọn hơn, chủ shop khỏi phải tạo khu vực.

Anh em cần phí ship thật của GHTK, GHN hay Viettel Post mà không muốn tự viết phần gọi API thì xem các plugin vận chuyển cho WooCommerce của mình. Còn tự viết mà vướng chỗ nào thì để lại bình luận nhé.

Sản phẩm nổi bật của Toản
x