Skip to content

Instantly share code, notes, and snippets.

@mirite
Created April 10, 2022 14:13
Show Gist options
  • Select an option

  • Save mirite/d25802141ebc7e618255ac323abc82c1 to your computer and use it in GitHub Desktop.

Select an option

Save mirite/d25802141ebc7e618255ac323abc82c1 to your computer and use it in GitHub Desktop.
Have a product in WooCommerce take its inventory from another product
<?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