Created
April 10, 2022 14:13
-
-
Save mirite/d25802141ebc7e618255ac323abc82c1 to your computer and use it in GitHub Desktop.
Have a product in WooCommerce take its inventory from another product
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Variations Pull From Product Inventory | |
| * | |
| * @wordpress-plugin | |
| * Plugin Name: Variations Pull From Product Inventory | |
| * Description: A WooCommerce plugin to allow variations to pull from another product's inventory. | |
| * Version: 1.3.0 | |
| * Requires at least: 5.2 | |
| * Requires PHP: 7.2 | |
| * Author: Jesse Conner | |
| * License URI: http://www.gnu.org/licenses/gpl-3.0.txt | |
| * | |
| * @package Variations Pull From Product Inventory | |
| */ | |
| /** | |
| * Allows for products that pull from the inventory of other products. | |
| * | |
| * @since 1.0.0 | |
| */ | |
| class Pull_From_Other_Inventory { | |
| private const SOURCE_META_FIELD = 'my_source_meta_field'; | |
| private const AMOUNT_META_FIELD = 'my_amount_meta_field'; | |
| /** | |
| * Instantiate the hooks and filters of our class | |
| * | |
| * @return void | |
| * @since 1.0.0 | |
| */ | |
| public function __construct() { | |
| add_action( 'woocommerce_product_after_variable_attributes', array( $this, 'variation_settings' ), 10, 3 ); | |
| add_action( 'woocommerce_save_product_variation', array( $this, 'save_fields' ), 10, 2 ); | |
| add_action( 'woocommerce_product_options_general_product_data', array( $this, 'product_settings' ) ); | |
| add_action( 'woocommerce_process_product_meta', array( $this, 'save_fields' ), 10, 1 ); | |
| add_action( 'woocommerce_thankyou', array( $this, 'update_stock' ) ); | |
| add_action( 'woocommerce_product_set_stock', array( $this, 'sync_stock' ), 10, 1 ); | |
| add_action( 'woocommerce_check_cart_items', array( $this, 'validate_stock' ), 10 ); | |
| } | |
| /** | |
| * Add the source product settings to variations | |
| * | |
| * @param WP_Query $loop the existing query loop. | |
| * @param WC_Variation $variation_data not actually used, but needed for the hook. | |
| * @param WC_Variation $variation the variation object to show the settings for. | |
| * | |
| * @return void | |
| * @since 1.2.0 | |
| */ | |
| public function variation_settings( $loop, $variation_data, $variation ) { | |
| $source = get_post_meta( $variation->ID, self::SOURCE_META_FIELD, true ); | |
| $amount = get_post_meta( $variation->ID, self::AMOUNT_META_FIELD, true ); | |
| $source_options = array(); | |
| $args = array( | |
| 'post_type' => 'product', | |
| 'posts_per_page' => 1000, | |
| 'meta_query' => array( | |
| array( | |
| 'key' => '_stock', | |
| 'value' => 0, | |
| 'compare' => '>', | |
| 'type' => 'NUMERIC', | |
| ), | |
| ), | |
| ); | |
| $loop = new WP_Query( $args ); | |
| if ( ! $loop->have_posts() ) { | |
| $source_options[0] = 'None Available'; | |
| } else { | |
| $source_options[0] = 'None'; | |
| } | |
| while ( $loop->have_posts() ) : | |
| $loop->the_post(); | |
| global $product; | |
| $source_options[ get_the_id() ] = get_the_title(); | |
| endwhile; | |
| wp_reset_postdata(); | |
| ?> | |
| <div class="option_group"> | |
| <?php | |
| woocommerce_wp_select( | |
| array( | |
| 'id' => self::SOURCE_META_FIELD, | |
| 'label' => 'Takes inventory from:', | |
| 'value' => $source, | |
| 'options' => $source_options, | |
| 'wrapper_class' => 'form-field-wide', | |
| ) | |
| ); | |
| woocommerce_wp_select( | |
| array( | |
| 'id' => self::AMOUNT_META_FIELD, | |
| 'label' => 'Quantity to take:', | |
| 'value' => $amount, | |
| 'options' => array( | |
| '0' => '0', | |
| '1' => '1', | |
| '2' => '2', | |
| '3' => '3', | |
| '4' => '4', | |
| '5' => '5', | |
| '6' => '6', | |
| '7' => '7', | |
| '8' => '8', | |
| '9' => '9', | |
| '10' => '10', | |
| ), | |
| 'wrapper_class' => 'form-field-wide', | |
| ) | |
| ); | |
| wp_nonce_field( 'save', 'pull-from-inventory' ); | |
| ?> | |
| </div> | |
| <?php | |
| } | |
| /** | |
| * Add the source product settings to simple products | |
| * | |
| * @return void | |
| * @since 1.2.0 | |
| */ | |
| public function product_settings() { | |
| $product = new WC_Product( get_the_ID() ); | |
| if ( $product->is_type( 'variable' ) ) { | |
| // Only show on simple products to avoid a lot of confusion. | |
| return; | |
| } | |
| $source = get_post_meta( get_the_ID(), self::SOURCE_META_FIELD, true ); | |
| $amount = get_post_meta( get_the_ID(), self::AMOUNT_META_FIELD, true ); | |
| $source_options = array(); | |
| $args = array( | |
| 'post_type' => 'product', | |
| 'posts_per_page' => 1000, | |
| 'meta_query' => array( | |
| array( | |
| 'key' => '_stock', | |
| 'value' => 0, | |
| 'compare' => '>', | |
| 'type' => 'NUMERIC', | |
| ), | |
| ), | |
| ); | |
| $loop = new WP_Query( $args ); | |
| if ( ! $loop->have_posts() ) { | |
| $source_options[0] = 'None Available'; | |
| } else { | |
| $source_options[0] = 'None'; | |
| } | |
| while ( $loop->have_posts() ) : | |
| $loop->the_post(); | |
| $source_options[ get_the_id() ] = get_the_title(); | |
| endwhile; | |
| wp_reset_postdata(); | |
| ?> | |
| <div class="option_group"> | |
| <?php | |
| woocommerce_wp_select( | |
| array( | |
| 'id' => self::SOURCE_META_FIELD, | |
| 'label' => 'Takes inventory from:', | |
| 'value' => $source, | |
| 'options' => $source_options, | |
| 'wrapper_class' => 'form-field-wide', | |
| ) | |
| ); | |
| woocommerce_wp_select( | |
| array( | |
| 'id' => self::AMOUNT_META_FIELD, | |
| 'label' => 'Quantity to take:', | |
| 'value' => $amount, | |
| 'options' => array( | |
| '0' => '0', | |
| '1' => '1', | |
| '2' => '2', | |
| '3' => '3', | |
| '4' => '4', | |
| '5' => '5', | |
| '6' => '6', | |
| '7' => '7', | |
| '8' => '8', | |
| '9' => '9', | |
| '10' => '10', | |
| ), | |
| 'wrapper_class' => 'form-field-wide', | |
| ) | |
| ); | |
| wp_nonce_field( 'save', 'pull-from-inventory' ); | |
| ?> | |
| </div> | |
| <?php | |
| } | |
| /** | |
| * Save the product source settings | |
| * | |
| * @param int $id the id of the product whose meta needs updating. | |
| * | |
| * @return void | |
| * @since 1.0.0 | |
| */ | |
| public function save_fields( $id ) { | |
| if ( isset( $_POST[self::SOURCE_META_FIELD], $_POST[self::AMOUNT_META_FIELD] ) && check_admin_referer( 'save', 'pull-from-inventory' ) ) { | |
| $source = sanitize_meta( self::SOURCE_META_FIELD, wp_unslash( $_POST[self::SOURCE_META_FIELD] ), 'post', 'product' ); | |
| $amount = sanitize_meta( self::AMOUNT_META_FIELD, wp_unslash( $_POST[self::AMOUNT_META_FIELD] ), 'post', 'product' ); | |
| } else { | |
| return; | |
| } | |
| update_post_meta( $id, self::SOURCE_META_FIELD, esc_attr( $source ) ); | |
| update_post_meta( $id, self::AMOUNT_META_FIELD, esc_attr( $amount ) ); | |
| } | |
| /** | |
| * Updates the stock of source items on order marked as complete | |
| * | |
| * @param int $order_id The id of the order that affects the source stock. | |
| * | |
| * @return void | |
| * @since 1.0.0 | |
| */ | |
| public function update_stock( $order_id ) { | |
| if ( get_post_meta( $order_id, 'stock_updated', true ) ) { | |
| return; | |
| } | |
| $order = wc_get_order( $order_id ); | |
| $products = $order->get_items(); | |
| foreach ( $products as $prod ) { | |
| if ( $prod['variation_id'] ) { | |
| $id = $prod['variation_id']; | |
| } else { | |
| $id = $prod['product_id']; | |
| } | |
| if ( ! $id ) { | |
| continue; | |
| } | |
| $source = get_post_meta( $id, self::SOURCE_META_FIELD, true ); | |
| $amount = get_post_meta( $id, self::AMOUNT_META_FIELD, true ) * $prod['quantity']; | |
| if ( $source > 0 && $amount > 0 ) { | |
| wc_update_product_stock( $source, $amount, 'decrease' ); | |
| } | |
| } | |
| update_post_meta( $order_id, 'stock_updated', true ); | |
| } | |
| /** | |
| * Updates the stock of the child products when a source has been updated | |
| * | |
| * @param WC_Product $source_product the product object that needs its children updated. | |
| * | |
| * @return void | |
| * @since 1.0.0 | |
| */ | |
| public function sync_stock( $source_product ) { | |
| $args = array( | |
| 'post_type' => array( | |
| 'product', | |
| 'product_variation', | |
| ), | |
| 'posts_per_page' => 100000, | |
| 'meta_query' => array( | |
| array( | |
| 'key' => self::SOURCE_META_FIELD, | |
| 'value' => $source_product->get_id(), | |
| 'compare' => '=', | |
| 'type' => 'NUMERIC', | |
| ), | |
| ), | |
| ); | |
| $loop = new WP_Query( $args ); | |
| while ( $loop->have_posts() ) : | |
| $loop->the_post(); | |
| $unit_qty = get_post_meta( get_the_id(), self::AMOUNT_META_FIELD, true ); | |
| $qty = $source_product->get_stock_quantity(); | |
| $amount = floor( $qty / $unit_qty ); | |
| if ( ! $amount ) { | |
| $amount = 0; | |
| } | |
| wc_update_product_stock( get_the_ID(), $amount ); | |
| endwhile; | |
| wp_reset_postdata(); | |
| } | |
| /** | |
| * Validates the stock availability at checkout | |
| * | |
| * @return void | |
| * @since 1.1.0 | |
| */ | |
| public function validate_stock() { | |
| $totals = array(); | |
| foreach ( WC()->cart->cart_contents as $cart_content_product ) { | |
| $id = $cart_content_product['variation_id'] > 0 ? $cart_content_product['variation_id'] : $cart_content_product['product_id']; | |
| $source = get_post_meta( $id, self::SOURCE_META_FIELD, true ); | |
| $amount = get_post_meta( $id, self::AMOUNT_META_FIELD, true ); | |
| if ( $source && $amount ) : | |
| // Inventory belongs to a source. | |
| $qty = $cart_content_product['quantity']; | |
| $totals[ $source ] += $amount * $qty; | |
| endif; | |
| } | |
| foreach ( $totals as $controlled_item => $amount ) { | |
| $source_product = wc_get_product( $controlled_item ); | |
| if ( $amount > $source_product->get_stock_quantity() ) { | |
| wc_add_notice( '<strong>There is not enough stock to complete the purchase</strong>', 'error' ); | |
| } | |
| } | |
| } | |
| } | |
| // Make sure WooCommerce is active before instantiating the plugin. | |
| if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ), true ) ) { | |
| // Finally, instantiate our plugin class. | |
| new Pull_From_Other_Inventory(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment