ase_stock_levels( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return; } $stock_reduced = $order->get_data_store()->get_stock_reduced( $order_id ); $trigger_increase = (bool) $stock_reduced; // Only continue if we're increasing stock. if ( ! $trigger_increase ) { return; } wc_increase_stock_levels( $order ); // Ensure stock is not marked as "reduced" anymore. $order->get_data_store()->set_stock_reduced( $order_id, false ); } add_action( 'woocommerce_order_status_cancelled', 'wc_maybe_increase_stock_levels' ); add_action( 'woocommerce_order_status_pending', 'wc_maybe_increase_stock_levels' ); /** * Reduce stock levels for items within an order, if stock has not already been reduced for the items. * * @since 3.0.0 * @param int|WC_Order $order_id Order ID or order instance. */ function wc_reduce_stock_levels( $order_id ) { if ( is_a( $order_id, 'WC_Order' ) ) { $order = $order_id; $order_id = $order->get_id(); } else { $order = wc_get_order( $order_id ); } // We need an order, and a store with stock management to continue. if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_reduce_order_stock', true, $order ) ) { return; } $changes = array(); // Loop over all items. foreach ( $order->get_items() as $item ) { if ( ! $item->is_type( 'line_item' ) ) { continue; } // Only reduce stock once for each item. $product = $item->get_product(); $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); if ( $item_stock_reduced || ! $product || ! $product->managing_stock() ) { continue; } /** * Filter order item quantity. * * @param int|float $quantity Quantity. * @param WC_Order $order Order data. * @param WC_Order_Item_Product $item Order item data. */ $qty = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item ); $item_name = $product->get_formatted_name(); $new_stock = wc_update_product_stock( $product, $qty, 'decrease' ); if ( is_wp_error( $new_stock ) ) { /* translators: %s item name. */ $order->add_order_note( sprintf( __( 'Unable to reduce stock for item %s.', 'woocommerce' ), $item_name ) ); continue; } $item->add_meta_data( '_reduced_stock', $qty, true ); $item->save(); $change = array( 'product' => $product, 'from' => $new_stock + $qty, 'to' => $new_stock, ); $changes[] = $change; /** * Fires when stock reduced to a specific line item * * @param WC_Order_Item_Product $item Order item data. * @param array $change Change Details. * @param WC_Order $order Order data. * @since 7.6.0 */ do_action( 'woocommerce_reduce_order_item_stock', $item, $change, $order ); } wc_trigger_stock_change_notifications( $order, $changes ); do_action( 'woocommerce_reduce_order_stock', $order ); } /** * After stock change events, triggers emails and adds order notes. * * @since 3.5.0 * @param WC_Order $order order object. * @param array $changes Array of changes. */ function wc_trigger_stock_change_notifications( $order, $changes ) { if ( empty( $changes ) ) { return; } $order_notes = array(); $no_stock_amount = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) ); foreach ( $changes as $change ) { $order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to']; $low_stock_amount = absint( wc_get_low_stock_amount( wc_get_product( $change['product']->get_id() ) ) ); if ( $change['to'] <= $no_stock_amount ) { do_action( 'woocommerce_no_stock', wc_get_product( $change['product']->get_id() ) ); } elseif ( $change['to'] <= $low_stock_amount ) { do_action( 'woocommerce_low_stock', wc_get_product( $change['product']->get_id() ) ); } if ( $change['to'] < 0 ) { do_action( 'woocommerce_product_on_backorder', array( 'product' => wc_get_product( $change['product']->get_id() ), 'order_id' => $order->get_id(), 'quantity' => abs( $change['from'] - $change['to'] ), ) ); } } $order->add_order_note( __( 'Stock levels reduced:', 'woocommerce' ) . ' ' . implode( ', ', $order_notes ) ); } /** * Increase stock levels for items within an order. * * @since 3.0.0 * @param int|WC_Order $order_id Order ID or order instance. */ function wc_increase_stock_levels( $order_id ) { if ( is_a( $order_id, 'WC_Order' ) ) { $order = $order_id; $order_id = $order->get_id(); } else { $order = wc_get_order( $order_id ); } // We need an order, and a store with stock management to continue. if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_restore_order_stock', true, $order ) ) { return; } $changes = array(); // Loop over all items. foreach ( $order->get_items() as $item ) { if ( ! $item->is_type( 'line_item' ) ) { continue; } // Only increase stock once for each item. $product = $item->get_product(); $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); if ( ! $item_stock_reduced || ! $product || ! $product->managing_stock() ) { continue; } $item_name = $product->get_formatted_name(); $new_stock = wc_update_product_stock( $product, $item_stock_reduced, 'increase' ); if ( is_wp_error( $new_stock ) ) { /* translators: %s item name. */ $order->add_order_note( sprintf( __( 'Unable to restore stock for item %s.', 'woocommerce' ), $item_name ) ); continue; } $item->delete_meta_data( '_reduced_stock' ); $item->save(); $changes[] = $item_name . ' ' . ( $new_stock - $item_stock_reduced ) . '→' . $new_stock; } if ( $changes ) { $order->add_order_note( __( 'Stock levels increased:', 'woocommerce' ) . ' ' . implode( ', ', $changes ) ); } do_action( 'woocommerce_restore_order_stock', $order ); } /** * See how much stock is being held in pending orders. * * @since 3.5.0 * @param WC_Product $product Product to check. * @param integer $exclude_order_id Order ID to exclude. * @return int */ function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0 ) { /** * Filter: woocommerce_hold_stock_for_checkout * Allows enable/disable hold stock functionality on checkout. * * @since 4.3.0 * @param bool $enabled Default to true if managing stock globally. */ if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { return 0; } return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id ); } /** * Hold stock for an order. * * @throws ReserveStockException If reserve stock fails. * * @since 4.1.0 * @param \WC_Order|int $order Order ID or instance. */ function wc_reserve_stock_for_order( $order ) { /** * Filter: woocommerce_hold_stock_for_checkout * Allows enable/disable hold stock functionality on checkout. * * @since @since 4.1.0 * @param bool $enabled Default to true if managing stock globally. */ if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { return; } $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); if ( $order ) { ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->reserve_stock_for_order( $order ); } } add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' ); /** * Release held stock for an order. * * @since 4.3.0 * @param \WC_Order|int $order Order ID or instance. */ function wc_release_stock_for_order( $order ) { /** * Filter: woocommerce_hold_stock_for_checkout * Allows enable/disable hold stock functionality on checkout. * * @since 4.3.0 * @param bool $enabled Default to true if managing stock globally. */ if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { return; } $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); if ( $order ) { ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->release_stock_for_order( $order ); } } add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' ); add_action( 'woocommerce_payment_complete', 'wc_release_stock_for_order', 11 ); add_action( 'woocommerce_order_status_cancelled', 'wc_release_stock_for_order', 11 ); add_action( 'woocommerce_order_status_completed', 'wc_release_stock_for_order', 11 ); add_action( 'woocommerce_order_status_processing', 'wc_release_stock_for_order', 11 ); add_action( 'woocommerce_order_status_on-hold', 'wc_release_stock_for_order', 11 ); /** * Return low stock amount to determine if notification needs to be sent * * Since 5.2.0, this function no longer redirects from variation to its parent product. * Low stock amount can now be attached to the variation itself and if it isn't, only * then we check the parent product, and if it's not there, then we take the default * from the store-wide setting. * * @param WC_Product $product Product to get data from. * @since 3.5.0 * @return int */ function wc_get_low_stock_amount( WC_Product $product ) { $low_stock_amount = $product->get_low_stock_amount(); if ( '' === $low_stock_amount && $product->is_type( 'variation' ) ) { $product = wc_get_product( $product->get_parent_id() ); $low_stock_amount = $product->get_low_stock_amount(); } if ( '' === $low_stock_amount ) { $low_stock_amount = get_option( 'woocommerce_notify_low_stock_amount', 2 ); } return (int) $low_stock_amount; } ce 1.7 * @author Grégory Viguier * @access public * * @return array The options. */ public function get_all() { $values = $this->get_raw(); if ( ! $values ) { return $this->get_reset_values(); } return imagify_merge_intersect( $values, $this->get_default_values() ); } /** * Set one or multiple options. * * @since 1.7 * @author Grégory Viguier * @access public * * @param array $values An array of option name / option value pairs. */ public function set( $values ) { $args = func_get_args(); if ( isset( $args[1] ) && is_string( $args[0] ) ) { $values = array( $args[0] => $args[1] ); } if ( ! is_array( $values ) ) { // PABKAC. return; } $values = array_merge( $this->get_all(), $values ); $values = array_intersect_key( $values, $this->get_default_values() ); $this->set_raw( $values ); } /** * Delete one or multiple options. * * @since 1.7 * @author Grégory Viguier * @access public * * @param array|string $keys An array of option names or a single option name. */ public function delete( $keys ) { $values = $this->get_raw(); if ( ! $values ) { if ( false !== $values ) { $this->delete_raw(); } return; } $keys = array_flip( (array) $keys ); $values = array_diff_key( $values, $keys ); $this->set_raw( $values ); } /** * Checks if the option with the given name exists or not. * * @since 1.7 * @author Grégory Viguier * @access public * * @param string $key The option name. * @return bool */ public function has( $key ) { return null !== $this->get( $key ); } /** ----------------------------------------------------------------------------------------- */ /** GET / UPDATE / DELETE RAW VALUES ======================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get the name of the option that stores the settings. * * @since 1.7 * @author Grégory Viguier * @access public * * @return string */ public function get_option_name() { return IMAGIFY_SLUG . '_' . $this->identifier; } /** * Get the identifier used in the hook names. * * @since 1.7 * @author Grégory Viguier * @access public * * @return string */ public function get_hook_identifier() { return $this->hook_identifier; } /** * Tell if the option is autoloaded. * * @since 1.7 * @author Grégory Viguier * @access public * * @return bool */ public function is_autoloaded() { return 'yes' === $this->autoload; } /** * Tell if the option is a network option. * * @since 1.7 * @author Grégory Viguier * @access public * * @return bool */ public function is_network_option() { return (bool) $this->network_option; } /** * Get the raw value of all Imagify options. * * @since 1.7 * @author Grégory Viguier * @access public * * @return array|bool The options. False if not set yet. An empty array if invalid. */ public function get_raw() { $values = $this->is_network_option() ? get_site_option( $this->get_option_name() ) : get_option( $this->get_option_name() ); if ( false !== $values && ! is_array( $values ) ) { return array(); } return $values; } /** * Update the Imagify options. * * @since 1.7 * @author Grégory Viguier * @access public * * @param array $values An array of option name / option value pairs. */ public function set_raw( $values ) { if ( ! $values ) { // The option is empty: delete it. $this->delete_raw(); } elseif ( $this->is_network_option() ) { // Network option. update_site_option( $this->get_option_name(), $values ); } elseif ( false === get_option( $this->get_option_name() ) ) { // Compat' with WP < 4.2 + autoload: the option doesn't exist in the database. add_option( $this->get_option_name(), $values, '', $this->autoload ); } else { // Update the current value. update_option( $this->get_option_name(), $values, $this->autoload ); } } /** * Delete all Imagify options. * * @since 1.7 * @author Grégory Viguier * @access public */ public function delete_raw() { $this->is_network_option() ? delete_site_option( $this->get_option_name() ) : delete_option( $this->get_option_name() ); } /** ----------------------------------------------------------------------------------------- */ /** DEFAULT + RESET VALUES ================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Get default option values. * * @since 1.7 * @author Grégory Viguier * @access public * * @return array */ public function get_default_values() { $default_values = $this->default_values; if ( ! empty( $default_values['cached'] ) ) { unset( $default_values['cached'] ); return $default_values; } /** * Allow to add more default option values. * * @since 1.7 * @author Grégory Viguier * * @param array $new_values New default option values. * @param array $default_values Plugin default option values. */ $new_values = apply_filters( 'imagify_default_' . $this->get_hook_identifier() . '_values', array(), $default_values ); $new_values = is_array( $new_values ) ? $new_values : array(); if ( $new_values ) { // Don't allow new values to overwrite the plugin values. $new_values = array_diff_key( $new_values, $default_values ); } if ( $new_values ) { $default_values = array_merge( $default_values, $new_values ); $this->default_values = $default_values; } $this->default_values['cached'] = 1; return $default_values; } /** * Get the values used when the option is empty. * * @since 1.7 * @author Grégory Viguier * @access public * * @return array */ public function get_reset_values() { $reset_values = $this->reset_values; if ( ! empty( $reset_values['cached'] ) ) { unset( $reset_values['cached'] ); return $reset_values; } $default_values = $this->get_default_values(); $reset_values = array_merge( $default_values, $reset_values ); /** * Allow to filter the "reset" option values. * * @since 1.7 * @author Grégory Viguier * * @param array $reset_values Plugin reset option values. */ $new_values = apply_filters( 'imagify_reset_' . $this->get_hook_identifier() . '_values', $reset_values ); if ( $new_values && is_array( $new_values ) ) { $reset_values = array_merge( $reset_values, $new_values ); } $this->reset_values = $reset_values; $this->reset_values['cached'] = 1; return $reset_values; } /** ----------------------------------------------------------------------------------------- */ /** SANITIZATION, VALIDATION ================================================================ */ /** ----------------------------------------------------------------------------------------- */ /** * Sanitize and validate an option value. * * @since 1.7 * @author Grégory Viguier * @access public * * @param string $key The option key. * @param mixed $value The value. * @param mixed $default The default value. * @return mixed */ public function sanitize_and_validate( $key, $value, $default = null ) { if ( ! isset( $default ) ) { $default_values = $this->get_default_values(); $default = $default_values[ $key ]; } // Cast the value. $value = self::cast( $value, $default ); if ( $value === $default ) { return $value; } // Version. if ( 'version' === $key ) { return sanitize_text_field( $value ); } return $this->sanitize_and_validate_value( $key, $value, $default ); } /** * Sanitize and validate an option value. Basic casts have been made. * * @since 1.7 * @author Grégory Viguier * @access public * * @param string $key The option key. * @param mixed $value The value. * @param mixed $default The default value. * @return mixed */ abstract public function sanitize_and_validate_value( $key, $value, $default ); /** * Sanitize and validate Imagify's options before storing them. * * @since 1.7 * @author Grégory Viguier * @access public * * @param string $values The option value. * @return array */ public function sanitize_and_validate_on_update( $values ) { $values = is_array( $values ) ? $values : array(); $default_values = $this->get_default_values(); if ( $values ) { foreach ( $default_values as $key => $default ) { if ( isset( $values[ $key ] ) ) { $values[ $key ] = $this->sanitize_and_validate( $key, $values[ $key ], $default ); } } } $values = array_intersect_key( $values, $default_values ); // Version. if ( empty( $values['version'] ) ) { $values['version'] = IMAGIFY_VERSION; } return $this->validate_values_on_update( $values ); } /** * Validate Imagify's options before storing them. Basic sanitization and validation have been made, row by row. * * @since 1.7 * @author Grégory Viguier * @access public * * @param string $values The option value. * @return array */ public function validate_values_on_update( $values ) { return $values; } /** ----------------------------------------------------------------------------------------- */ /** TOOLS =================================================================================== */ /** ----------------------------------------------------------------------------------------- */ /** * Cast a value, depending on its default value type. * * @since 1.7 * @author Grégory Viguier * @access public * * @param mixed $value The value to cast. * @param mixed $default The default value. * @return mixed */ public static function cast( $value, $default ) { if ( is_array( $default ) ) { return is_array( $value ) ? $value : array(); } if ( is_int( $default ) ) { return (int) $value; } if ( is_bool( $default ) ) { return (bool) $value; } if ( is_float( $default ) ) { return round( (float) $value, 3 ); } return $value; } /** * Cast a float like 3.000 into an integer. * * @since 1.7 * @author Grégory Viguier * @access public * * @param float $float The float. * @return float|int */ public static function maybe_cast_float_as_int( $float ) { return ( $float / (int) $float ) === (float) 1 ? (int) $float : $float; } }