From 1592b304b3bb169407e0432ee17c20c4a6ae80a9 Mon Sep 17 00:00:00 2001 From: vendidero Date: Fri, 28 Jun 2024 15:01:29 +0200 Subject: [PATCH] Improved bundles compatibility by: - Storing parent/child hierarchy within shipment items - Filtering product data while calculating order-related packaging data and syncing items with the shipment --- src/Compatibility/Bundles.php | 143 ++++++++++++++++++++++++++ src/DataStores/Shipment.php | 11 +- src/Interfaces/Compatibility.php | 20 ++++ src/Order.php | 49 +++++---- src/Package.php | 18 ++++ src/Packing/CartItem.php | 10 +- src/Packing/Item.php | 11 +- src/Packing/OrderItem.php | 8 +- src/Packing/Packer.php | 6 +- src/Packing/ShipmentItem.php | 2 +- src/Shipment.php | 1 + src/ShipmentItem.php | 30 ++++-- src/ShippingMethod/MethodHelper.php | 30 ++---- src/ShippingMethod/ShippingMethod.php | 24 ++++- 14 files changed, 290 insertions(+), 73 deletions(-) create mode 100644 src/Compatibility/Bundles.php create mode 100644 src/Interfaces/Compatibility.php diff --git a/src/Compatibility/Bundles.php b/src/Compatibility/Bundles.php new file mode 100644 index 00000000..88be3ce0 --- /dev/null +++ b/src/Compatibility/Bundles.php @@ -0,0 +1,143 @@ +get_items() as $item ) { + if ( $order_item = $item->get_order_item() ) { + $map[ $item->get_order_item_id() ] = $item->get_id(); + + if ( wc_pb_is_bundled_order_item( $order_item ) ) { + $container_id = wc_pb_get_bundled_order_item_container( $order_item, false, true ); + + if ( ! isset( $parents[ $container_id ] ) ) { + $parents[ $container_id ] = array(); + } + + $parents[ $container_id ][] = $item; + } + } + } + + foreach ( $parents as $order_item_id => $shipment_items ) { + if ( array_key_exists( $order_item_id, $map ) ) { + $parent_id = $map[ $order_item_id ]; + + foreach ( $shipment_items as $shipment_item ) { + $shipment_item->set_parent_id( $parent_id ); + } + } + } + } + + /** + * @param Product $product + * @param \WC_Order_Item_Product $item + * + * @return Product + */ + public static function get_product_from_item( $product, $item ) { + if ( ! $order = $item->get_order() ) { + return $product; + } + + $reset_shipping_props = false; + + if ( wc_pb_is_bundle_container_order_item( $item ) ) { + if ( $product->needs_shipping() ) { + if ( $bundle_weight = $item->get_meta( '_bundle_weight', true ) ) { + if ( is_null( $bundle_weight ) ) { + $bundle_weight = ''; + } + + $product->set_weight( $bundle_weight ); + } + } else { + $reset_shipping_props = true; + } + } elseif ( wc_pb_is_bundled_order_item( $item, $order ) ) { + if ( $product->needs_shipping() ) { + if ( 'no' === $item->get_meta( '_bundled_item_needs_shipping', true ) ) { + $reset_shipping_props = true; + } + } else { + $reset_shipping_props = true; + } + } + + if ( $reset_shipping_props ) { + $product->set_weight( 0 ); + $product->set_shipping_width( 0 ); + $product->set_shipping_height( 0 ); + $product->set_shipping_length( 0 ); + } + + return $product; + } + + /** + * Product Bundles cart item compatibility: + * In case the current item belongs to a parent bundle item (which contains the actual price) + * copy the pricing data from the parent once, e.g. for the first bundled item. + * + * @param $item + * @param $content_key + * + * @return mixed + */ + public static function adjust_cart_item( $item, $content_key ) { + if ( isset( $item['bundled_by'] ) && 0.0 === (float) $item['line_total'] && function_exists( 'wc_pb_get_bundled_cart_item_container' ) ) { + $bundled_by = $item['bundled_by']; + + if ( ! in_array( $bundled_by, self::$cart_bundled_by_map, true ) ) { + if ( $container = wc_pb_get_bundled_cart_item_container( $item ) ) { + $item['line_total'] = (float) $container['line_total']; + $item['line_subtotal'] = (float) $container['line_subtotal']; + $item['line_tax'] = (float) $container['line_tax']; + $item['line_subtotal_tax'] = (float) $container['line_subtotal_tax']; + + self::$cart_bundled_by_map[] = $bundled_by; + } + } + } + + return $item; + } +} diff --git a/src/DataStores/Shipment.php b/src/DataStores/Shipment.php index 28c01557..141f2e62 100644 --- a/src/DataStores/Shipment.php +++ b/src/DataStores/Shipment.php @@ -577,12 +577,17 @@ public function read_items( $shipment ) { } if ( ! empty( $items ) ) { - $shipment_type = $shipment->get_type(); $items = array_map( - function( $item_id ) use ( $shipment_type ) { - return wc_gzd_get_shipment_item( $item_id, $shipment_type ); + function( $item_id ) use ( $shipment_type, $shipment ) { + $item = wc_gzd_get_shipment_item( $item_id, $shipment_type ); + + if ( $item ) { + $item->set_shipment( $shipment ); + } + + return $item; }, array_combine( wp_list_pluck( $items, 'shipment_item_id' ), $items ) ); diff --git a/src/Interfaces/Compatibility.php b/src/Interfaces/Compatibility.php new file mode 100644 index 00000000..27223b52 --- /dev/null +++ b/src/Interfaces/Compatibility.php @@ -0,0 +1,20 @@ +get_product() ) ) { + $s_product = wc_gzd_shipments_get_product( $product ); + } + + return apply_filters( 'woocommerce_gzd_shipments_order_item_product', $s_product, $order_item ); + } + protected function get_package_data() { if ( is_null( $this->package_data ) ) { $items = $this->get_available_items_for_shipment(); @@ -231,19 +246,17 @@ protected function get_package_data() { $quantity = (int) $item['max_quantity']; - if ( $product = $order_item->get_product() ) { - $s_product = wc_gzd_shipments_get_product( $product ); - - $width = ( empty( $s_product->get_shipping_width() ) ? 0 : (float) wc_format_decimal( $s_product->get_shipping_width() ) ) * $quantity; - $length = ( empty( $s_product->get_shipping_length() ) ? 0 : (float) wc_format_decimal( $s_product->get_shipping_length() ) ) * $quantity; - $height = ( empty( $s_product->get_shipping_height() ) ? 0 : (float) wc_format_decimal( $s_product->get_shipping_height() ) ) * $quantity; - $weight = ( empty( $s_product->get_weight() ) ? 0 : (float) wc_format_decimal( $product->get_weight() ) ) * $quantity; + if ( $product = $this->get_order_item_product( $order_item ) ) { + $width = ( empty( $product->get_shipping_width() ) ? 0 : (float) wc_format_decimal( $product->get_shipping_width() ) ) * $quantity; + $length = ( empty( $product->get_shipping_length() ) ? 0 : (float) wc_format_decimal( $product->get_shipping_length() ) ) * $quantity; + $height = ( empty( $product->get_shipping_height() ) ? 0 : (float) wc_format_decimal( $product->get_shipping_height() ) ) * $quantity; + $weight = ( empty( $product->get_weight() ) ? 0 : (float) wc_format_decimal( $product->get_weight() ) ) * $quantity; $package_data['weight'] += $weight; $package_data['volume'] += ( $width * $length * $height ); - if ( $product && ! array_key_exists( $product->get_id(), $package_data['products'] ) ) { - $package_data['products'][ $product->get_id() ] = $product; + if ( ! array_key_exists( $product->get_id(), $package_data['products'] ) ) { + $package_data['products'][ $product->get_id() ] = $product->get_product(); if ( ! empty( $product->get_shipping_class_id() ) ) { $package_data['shipping_classes'][] = $product->get_shipping_class_id(); @@ -291,8 +304,7 @@ public function create_shipments( $default_status = 'processing' ) { } $items = $this->get_items_to_pack_left_for_shipping(); - $boxes = PackagingList::fromArray( $packaging_boxes ); - $packed_boxes = Helper::pack( $items, $boxes, 'order' ); + $packed_boxes = Helper::pack( $items, $packaging_boxes, 'order' ); if ( empty( $packaging_boxes ) && 0 === count( $packed_boxes ) ) { $shipment = wc_gzd_create_shipment( $this, array( 'props' => array( 'status' => $default_status ) ) ); @@ -682,8 +694,8 @@ public function order_item_is_non_returnable( $order_item_id ) { if ( $order_item ) { if ( is_callable( array( $order_item, 'get_product' ) ) ) { - if ( $product = $order_item->get_product() ) { - $is_non_returnable = wc_gzd_shipments_get_product( $product )->is_non_returnable(); + if ( $product = $this->get_order_item_product( $order_item ) ) { + $is_non_returnable = $product->is_non_returnable(); } } } @@ -759,7 +771,7 @@ public function get_items_to_pack_left_for_shipping( $legacy_group_by_product_gr $product_group = ''; - if ( $product = $order_item->get_product() ) { + if ( $product = $this->get_order_item_product( $order_item ) ) { $product_group = ''; if ( 'yes' === get_option( 'woocommerce_gzd_shipments_packing_group_by_shipping_class' ) ) { @@ -809,7 +821,7 @@ public function get_available_items_for_shipment( $args = array() ) { $sku = ''; if ( is_callable( array( $item, 'get_product' ) ) ) { - if ( $product = $item->get_product() ) { + if ( $product = $this->get_order_item_product( $item ) ) { $sku = $product->get_sku(); } } @@ -989,9 +1001,7 @@ public function get_shippable_items() { $items = $this->get_order()->get_items( 'line_item' ); foreach ( $items as $key => $item ) { - $product = is_callable( array( $item, 'get_product' ) ) ? $item->get_product() : false; - - if ( $product ) { + if ( $product = $this->get_order_item_product( $item ) ) { if ( $product->is_virtual() || $this->get_shippable_item_quantity( $item ) <= 0 ) { unset( $items[ $key ] ); } @@ -1011,6 +1021,9 @@ public function get_shippable_items() { * @since 3.0.0 * @package Vendidero/Germanized/Shipments */ + + do_action( 'woocommerce_gzd_shipments_order_after_get_items', $this->get_order() ); + return apply_filters( 'woocommerce_gzd_shipment_order_shippable_items', $items, $this->get_order(), $this ); } diff --git a/src/Package.php b/src/Package.php index 0c027fce..fc9ff0ad 100644 --- a/src/Package.php +++ b/src/Package.php @@ -36,6 +36,7 @@ public static function init() { self::maybe_set_upload_dir(); self::init_hooks(); self::includes(); + self::load_compatibilities(); do_action( 'woocommerce_gzd_shipments_init' ); } @@ -92,6 +93,23 @@ function ( $container ) { return $container; } + public static function load_compatibilities() { + $compatibilities = apply_filters( + 'woocommerce_gzd_shipments_compatibilities', + array( + 'bundles' => '\Vendidero\Germanized\Shipments\Compatibility\Bundles', + ) + ); + + foreach ( $compatibilities as $compatibility ) { + if ( is_a( $compatibility, '\Vendidero\Germanized\Shipments\Interfaces\Compatibility', true ) ) { + if ( $compatibility::is_active() ) { + $compatibility::init(); + } + } + } + } + public static function manipulate_shipping_rates( $args, $method ) { if ( $method = wc_gzd_get_shipping_provider_method( $method ) ) { $args['meta_data']['_shipping_provider'] = $method->get_shipping_provider(); diff --git a/src/Packing/CartItem.php b/src/Packing/CartItem.php index 4202f226..10711883 100644 --- a/src/Packing/CartItem.php +++ b/src/Packing/CartItem.php @@ -26,11 +26,9 @@ public function __construct( $item, $incl_taxes = false ) { throw new \Exception( 'Invalid item' ); } - $s_product = wc_gzd_shipments_get_product( $this->get_product() ); - - $width = empty( $s_product->get_shipping_width() ) ? 0 : (float) wc_format_decimal( $s_product->get_shipping_width() ); - $length = empty( $s_product->get_shipping_length() ) ? 0 : (float) wc_format_decimal( $s_product->get_shipping_length() ); - $depth = empty( $s_product->get_shipping_height() ) ? 0 : (float) wc_format_decimal( $s_product->get_shipping_height() ); + $width = empty( $this->get_product()->get_shipping_width() ) ? 0 : (float) wc_format_decimal( $this->get_product()->get_shipping_width() ); + $length = empty( $this->get_product()->get_shipping_length() ) ? 0 : (float) wc_format_decimal( $this->get_product()->get_shipping_length() ); + $depth = empty( $this->get_product()->get_shipping_height() ) ? 0 : (float) wc_format_decimal( $this->get_product()->get_shipping_height() ); $this->dimensions = array( 'width' => (int) wc_get_dimension( $width, 'mm' ), @@ -54,7 +52,7 @@ public function __construct( $item, $incl_taxes = false ) { } protected function load_product() { - $this->product = $this->item['data']; + $this->product = wc_gzd_shipments_get_product( $this->item['data'] ); } /** diff --git a/src/Packing/Item.php b/src/Packing/Item.php index d1d65ffd..9d971b7b 100644 --- a/src/Packing/Item.php +++ b/src/Packing/Item.php @@ -3,6 +3,7 @@ namespace Vendidero\Germanized\Shipments\Packing; use Vendidero\Germanized\Shipments\Interfaces\PackingItem; +use Vendidero\Germanized\Shipments\Product; defined( 'ABSPATH' ) || exit; @@ -36,7 +37,7 @@ protected function load_product() { } /** - * @return null|\WC_Product + * @return null|Product */ public function get_product() { if ( is_null( $this->product ) ) { @@ -99,9 +100,13 @@ public function getDescription(): string { $description = $this->get_id(); if ( $product = $this->get_product() ) { + $title = $product->get_title(); + if ( $product->get_sku() ) { $description = $this->get_product()->get_sku(); } + + $description = $title . ' (' . $description . ')'; } return apply_filters( 'woocommerce_gzd_packing_item_description', $description, $this ); @@ -135,6 +140,10 @@ public function getWeight(): int { return apply_filters( 'woocommerce_gzd_packing_item_weight_in_g', $this->weight, $this ); } + public function get_dimensions() { + return $this->dimensions; + } + /** * Item total in cents. * diff --git a/src/Packing/OrderItem.php b/src/Packing/OrderItem.php index 6eecea81..776c4120 100644 --- a/src/Packing/OrderItem.php +++ b/src/Packing/OrderItem.php @@ -25,9 +25,7 @@ public function __construct( $item ) { throw new \Exception( 'Invalid item' ); } - if ( $product = $this->get_product() ) { - $s_product = wc_gzd_shipments_get_product( $product ); - + if ( $s_product = $this->get_product() ) { $width = empty( $s_product->get_shipping_width() ) ? 0 : (float) wc_format_decimal( $s_product->get_shipping_width() ); $length = empty( $s_product->get_shipping_length() ) ? 0 : (float) wc_format_decimal( $s_product->get_shipping_length() ); $depth = empty( $s_product->get_shipping_height() ) ? 0 : (float) wc_format_decimal( $s_product->get_shipping_height() ); @@ -57,7 +55,9 @@ public function __construct( $item ) { } protected function load_product() { - $this->product = $this->item->get_product(); + if ( $product = $this->item->get_product() ) { + $this->product = apply_filters( 'woocommerce_gzd_shipments_order_item_product', wc_gzd_shipments_get_product( $product ), $this->item ); + } } public function get_id() { diff --git a/src/Packing/Packer.php b/src/Packing/Packer.php index d30520e1..18c12bec 100644 --- a/src/Packing/Packer.php +++ b/src/Packing/Packer.php @@ -17,13 +17,13 @@ public function set_max_boxes_to_balance_weight( $max_boxes ) { } public function set_boxes( $boxes ) { - if ( ! is_a( $boxes, '\DVDoug\BoxPacker\BoxList' ) ) { + if ( ! is_a( $boxes, 'Vendidero\Germanized\Shipments\Packing\PackagingList' ) ) { $first_box = ! empty( $boxes ) ? array_values( $boxes )[0] : false; if ( ! empty( $boxes ) && ! is_a( $first_box, 'Vendidero\Germanized\Shipments\Packing\PackagingBox' ) ) { - $boxes = \DVDoug\BoxPacker\BoxList::fromArray( Helper::get_packaging_boxes( $boxes ) ); + $boxes = PackagingList::fromArray( Helper::get_packaging_boxes( $boxes ) ); } else { - $boxes = \DVDoug\BoxPacker\BoxList::fromArray( $boxes ); + $boxes = PackagingList::fromArray( $boxes ); } } diff --git a/src/Packing/ShipmentItem.php b/src/Packing/ShipmentItem.php index 34a27301..90215043 100644 --- a/src/Packing/ShipmentItem.php +++ b/src/Packing/ShipmentItem.php @@ -60,6 +60,6 @@ public function get_id() { } protected function load_product() { - $this->product = $this->item->get_product(); + $this->product = wc_gzd_shipments_get_product( $this->item->get_product() ); } } diff --git a/src/Shipment.php b/src/Shipment.php index bdb9caff..fa01f778 100644 --- a/src/Shipment.php +++ b/src/Shipment.php @@ -2246,6 +2246,7 @@ public function add_item( $item ) { // Set parent. $item->set_shipment_id( $this->get_id() ); + $item->set_shipment( $this ); // Append new row with generated temporary ID. $item_id = $item->get_id(); diff --git a/src/ShipmentItem.php b/src/ShipmentItem.php index b3f4aacb..049243d0 100644 --- a/src/ShipmentItem.php +++ b/src/ShipmentItem.php @@ -377,8 +377,21 @@ public function sync( $args = array() ) { ) ); - $product = $this->get_product(); - $s_product = wc_gzd_shipments_get_product( $product ); + $product = null; + + if ( $shipment = $this->get_shipment() ) { + if ( $order_shipment = $shipment->get_order_shipment() ) { + $product = $order_shipment->get_order_item_product( $item ); + } + } + + if ( ! $product && is_callable( array( $item, 'get_product' ) ) ) { + if ( $product = $item->get_product() ) { + $product = wc_gzd_shipments_get_product( $product ); + } + + $product = apply_filters( 'woocommerce_gzd_shipments_order_item_product', $product, $item ); + } /** * Calculate the order item total per unit to make sure it is independent from @@ -410,12 +423,12 @@ public function sync( $args = array() ) { 'total' => $total + $tax_total, 'subtotal' => $subtotal + $tax_subtotal, 'weight' => $product ? wc_get_weight( $product->get_weight(), $shipment->get_weight_unit() ) : '', - 'length' => $s_product ? wc_get_dimension( (float) $s_product->get_shipping_length(), $shipment->get_dimension_unit() ) : '', - 'width' => $s_product ? wc_get_dimension( (float) $s_product->get_shipping_width(), $shipment->get_dimension_unit() ) : '', - 'height' => $s_product ? wc_get_dimension( (float) $s_product->get_shipping_height(), $shipment->get_dimension_unit() ) : '', - 'hs_code' => $s_product ? $s_product->get_hs_code() : '', - 'customs_description' => $s_product ? $s_product->get_customs_description() : '', - 'manufacture_country' => $s_product ? $s_product->get_manufacture_country() : '', + 'length' => $product ? wc_get_dimension( (float) $product->get_shipping_length(), $shipment->get_dimension_unit() ) : '', + 'width' => $product ? wc_get_dimension( (float) $product->get_shipping_width(), $shipment->get_dimension_unit() ) : '', + 'height' => $product ? wc_get_dimension( (float) $product->get_shipping_height(), $shipment->get_dimension_unit() ) : '', + 'hs_code' => $product ? $product->get_hs_code() : '', + 'customs_description' => $product ? $product->get_customs_description() : '', + 'manufacture_country' => $product ? $product->get_manufacture_country() : '', 'attributes' => $attributes, ) ); @@ -439,7 +452,6 @@ public function sync( $args = array() ) { public function get_order_item() { if ( is_null( $this->order_item ) && 0 < $this->get_order_item_id() ) { if ( $shipment = $this->get_shipment() ) { - if ( $order = $shipment->get_order() ) { $this->order_item = $order->get_item( $this->get_order_item_id() ); } diff --git a/src/ShippingMethod/MethodHelper.php b/src/ShippingMethod/MethodHelper.php index ec166d47..e3ebe832 100644 --- a/src/ShippingMethod/MethodHelper.php +++ b/src/ShippingMethod/MethodHelper.php @@ -78,32 +78,12 @@ public static function register_cart_items_to_pack( $cart_contents ) { 'item_count' => 0, ); - $items = new ItemList(); - $bundled_by_map = array(); + $items = new ItemList(); - foreach ( $content['contents'] as $content_key => $item ) { - $item = apply_filters( 'woocommerce_gzd_shipments_cart_item', $item, $content_key ); - - /** - * Product Bundles cart item compatibility: - * In case the current item belongs to a parent bundle item (which contains the actual price) - * copy the pricing data from the parent once, e.g. for the first bundled item. - */ - if ( isset( $item['bundled_by'] ) && 0.0 === (float) $item['line_total'] && function_exists( 'wc_pb_get_bundled_cart_item_container' ) ) { - $bundled_by = $item['bundled_by']; - - if ( ! in_array( $bundled_by, $bundled_by_map, true ) ) { - if ( $container = wc_pb_get_bundled_cart_item_container( $item ) ) { - $item['line_total'] = (float) $container['line_total']; - $item['line_subtotal'] = (float) $container['line_subtotal']; - $item['line_tax'] = (float) $container['line_tax']; - $item['line_subtotal_tax'] = (float) $container['line_subtotal_tax']; - - $bundled_by_map[] = $bundled_by; - } - } - } + do_action( 'woocommerce_gzd_shipments_before_prepare_cart_contents' ); + foreach ( $content['contents'] as $content_key => $item ) { + $item = apply_filters( 'woocommerce_gzd_shipments_cart_item', $item, $content_key ); $product = $item['data']; if ( ! is_a( $product, 'WC_Product' ) ) { @@ -143,6 +123,8 @@ public static function register_cart_items_to_pack( $cart_contents ) { $items->insert( $cart_item, $quantity ); } + do_action( 'woocommerce_gzd_shipments_after_prepare_cart_contents' ); + /** * In case prices have already been calculated, use the official * Woo API for better compatibility with extensions, e.g. Bundles. diff --git a/src/ShippingMethod/ShippingMethod.php b/src/ShippingMethod/ShippingMethod.php index a49f1e10..341a93f4 100644 --- a/src/ShippingMethod/ShippingMethod.php +++ b/src/ShippingMethod/ShippingMethod.php @@ -795,7 +795,23 @@ public function calculate_shipping( $package = array() ) { if ( $is_debug_mode && ! Package::is_constant_defined( 'WOOCOMMERCE_CHECKOUT' ) && ! Package::is_constant_defined( 'WC_DOING_AJAX' ) && ! empty( $debug_notices ) ) { $the_notice = ''; + $cart_wide_notice = ''; $available_box_list = array(); + $cart_wide_notices = array(); + + $cart_wide_notices[] = _x( '### Items available to pack:', 'shipments', 'woocommerce-germanized-shipments' ); + + foreach ( $package['items_to_pack'] as $item_to_pack ) { + $cart_wide_notices[] = $item_to_pack->getDescription() . ' (' . wc_gzd_format_shipment_dimensions( $item_to_pack->get_dimensions(), 'mm' ) . ', ' . wc_gzd_format_shipment_weight( $item_to_pack->getWeight(), 'g' ) . ')'; + } + + foreach ( $cart_wide_notices as $notice ) { + $cart_wide_notice .= $notice . '
'; + } + + if ( ! wc_has_notice( $cart_wide_notice ) ) { + wc_add_notice( $cart_wide_notice ); + } foreach ( $available_boxes as $box ) { $available_box_list[] = $box->get_packaging()->get_title(); @@ -1001,7 +1017,7 @@ protected function get_updated_cache() { $costs[ $packaging_id ] = array( 'min' => 0.0, 'max' => 0.0, - 'avg' => 0.0 + 'avg' => 0.0, ); foreach ( $packaging_rules as $packaging_rule ) { @@ -1043,9 +1059,9 @@ protected function get_updated_cache() { $costs[ $packaging_id ]['avg'] += $cost; } - if ( count( $packaging_rules ) > 0 ) { - $costs[ $packaging_id ]['avg'] = $costs[ $packaging_id ]['avg'] / count( $packaging_rules ); - } + if ( count( $packaging_rules ) > 0 ) { + $costs[ $packaging_id ]['avg'] = $costs[ $packaging_id ]['avg'] / count( $packaging_rules ); + } } $cache = array(