Merge pull request #29345 from woocommerce/hw/low-stock-threshold-for-variations

Low stock threshold for variations
This commit is contained in:
Néstor Soriano 2021-03-17 08:59:32 +01:00 committed by GitHub
commit 17224d0ab1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 273 additions and 8 deletions

View File

@ -763,6 +763,7 @@ jQuery( function( $ ) {
case 'variable_regular_price' :
case 'variable_sale_price' :
case 'variable_stock' :
case 'variable_low_stock_amount' :
case 'variable_weight' :
case 'variable_length' :
case 'variable_width' :

View File

@ -509,6 +509,7 @@ class WC_Meta_Box_Product_Data {
),
'manage_stock' => isset( $_POST['variable_manage_stock'][ $i ] ),
'stock_quantity' => $stock,
'low_stock_amount' => isset( $_POST['variable_low_stock_amount'][ $i ] ) && '' !== $_POST['variable_low_stock_amount'][ $i ] ? wc_stock_amount( wp_unslash( $_POST['variable_low_stock_amount'][ $i ] ) ) : '',
'backorders' => isset( $_POST['variable_backorders'], $_POST['variable_backorders'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_backorders'][ $i ] ) ) : null,
'stock_status' => isset( $_POST['variable_stock_status'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_stock_status'][ $i ] ) ) : null,
'image_id' => isset( $_POST['upload_image_id'][ $i ] ) ? wc_clean( wp_unslash( $_POST['upload_image_id'][ $i ] ) ) : null,

View File

@ -75,10 +75,14 @@ if ( ! defined( 'ABSPATH' ) ) {
array(
'id' => '_low_stock_amount',
'value' => $product_object->get_low_stock_amount( 'edit' ),
'placeholder' => get_option( 'woocommerce_notify_low_stock_amount' ),
'placeholder' => sprintf(
/* translators: %d: Amount of stock left */
esc_attr__( 'Store-wide threshold (%d)', 'woocommerce' ),
esc_attr( get_option( 'woocommerce_notify_low_stock_amount' ) )
),
'label' => __( 'Low stock threshold', 'woocommerce' ),
'desc_tip' => true,
'description' => __( 'When product stock reaches this amount you will be notified by email', 'woocommerce' ),
'description' => __( 'When product stock reaches this amount you will be notified by email. It is possible to define different values for each variation individually. The shop default value can be set in Settings > Products > Inventory.', 'woocommerce' ),
'type' => 'number',
'custom_attributes' => array(
'step' => 'any',

View File

@ -75,6 +75,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<option value="variable_stock_status_instock"><?php esc_html_e( 'Set Status - In stock', 'woocommerce' ); ?></option>
<option value="variable_stock_status_outofstock"><?php esc_html_e( 'Set Status - Out of stock', 'woocommerce' ); ?></option>
<option value="variable_stock_status_onbackorder"><?php esc_html_e( 'Set Status - On backorder', 'woocommerce' ); ?></option>
<option value="variable_low_stock_amount"><?php esc_html_e( 'Low stock threshold', 'woocommerce' ); ?></option>
</optgroup>
<optgroup label="<?php esc_attr_e( 'Shipping', 'woocommerce' ); ?>">
<option value="variable_length"><?php esc_html_e( 'Length', 'woocommerce' ); ?></option>

View File

@ -210,6 +210,35 @@ defined( 'ABSPATH' ) || exit;
)
);
$low_stock_placeholder = ( $product_object->get_manage_stock() && '' !== $product_object->get_low_stock_amount() )
? sprintf(
/* translators: %d: Amount of stock left */
esc_attr__( 'Parent product\'s threshold (%d)', 'woocommerce' ),
esc_attr( $product_object->get_low_stock_amount() )
)
: sprintf(
/* translators: %d: Amount of stock left */
esc_attr__( 'Store-wide threshold (%d)', 'woocommerce' ),
esc_attr( get_option( 'woocommerce_notify_low_stock_amount' ) )
);
woocommerce_wp_text_input(
array(
'id' => "variable_low_stock_amount{$loop}",
'name' => "variable_low_stock_amount[{$loop}]",
'value' => $variation_object->get_low_stock_amount( 'edit' ),
'placeholder' => $low_stock_placeholder,
'label' => __( 'Low stock threshold', 'woocommerce' ),
'desc_tip' => true,
'description' => __( 'When variation stock reaches this amount you will be notified by email. The default value for all variations can be set in the product Inventory tab. The shop default value can be set in Settings > Products > Inventory.', 'woocommerce' ),
'type' => 'number',
'custom_attributes' => array(
'step' => 'any',
),
'wrapper_class' => 'form-row',
)
);
/**
* Variation options inventory action.
*

View File

@ -2301,6 +2301,32 @@ class WC_AJAX {
}
}
/**
* Bulk action - Set Low Stock Amount.
*
* @param array $variations List of variations.
* @param array $data Data to set.
*
* @used-by bulk_edit_variations
*/
private static function variation_bulk_action_variable_low_stock_amount( $variations, $data ) {
if ( ! isset( $data['value'] ) ) {
return;
}
$low_stock_amount = wc_stock_amount( wc_clean( $data['value'] ) );
foreach ( $variations as $variation_id ) {
$variation = wc_get_product( $variation_id );
if ( $variation->managing_stock() ) {
$variation->set_low_stock_amount( $low_stock_amount );
} else {
$variation->set_low_stock_amount( '' );
}
$variation->save();
}
}
/**
* Bulk action - Set Weight.
*
@ -2547,6 +2573,7 @@ class WC_AJAX {
* @uses WC_AJAX::variation_bulk_action_toggle_virtual()
* @uses WC_AJAX::variation_bulk_action_toggle_downloadable()
* @uses WC_AJAX::variation_bulk_action_toggle_enabled
* @uses WC_AJAX::variation_bulk_action_variable_low_stock_amount()
*/
public static function bulk_edit_variations() {
ob_start();

View File

@ -357,6 +357,7 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
'date_on_sale_to' => get_post_meta( $id, '_sale_price_dates_to', true ),
'manage_stock' => get_post_meta( $id, '_manage_stock', true ),
'stock_status' => get_post_meta( $id, '_stock_status', true ),
'low_stock_amount' => get_post_meta( $id, '_low_stock_amount', true ),
'shipping_class_id' => current( $this->get_term_ids( $id, 'product_shipping_class' ) ),
'virtual' => get_post_meta( $id, '_virtual', true ),
'downloadable' => get_post_meta( $id, '_downloadable', true ),
@ -404,7 +405,6 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
'sku' => get_post_meta( $product->get_parent_id(), '_sku', true ),
'manage_stock' => get_post_meta( $product->get_parent_id(), '_manage_stock', true ),
'backorders' => get_post_meta( $product->get_parent_id(), '_backorders', true ),
'low_stock_amount' => get_post_meta( $product->get_parent_id(), '_low_stock_amount', true ),
'stock_quantity' => wc_stock_amount( get_post_meta( $product->get_parent_id(), '_stock', true ) ),
'weight' => get_post_meta( $product->get_parent_id(), '_weight', true ),
'length' => get_post_meta( $product->get_parent_id(), '_length', true ),

View File

@ -766,6 +766,9 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
case 'backordered':
$base_data['backordered'] = $product->is_on_backorder();
break;
case 'low_stock_amount':
$base_data['low_stock_amount'] = '' === $product->get_low_stock_amount() ? null : $product->get_low_stock_amount();
break;
case 'sold_individually':
$base_data['sold_individually'] = $product->is_sold_individually();
break;

View File

@ -65,6 +65,7 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
'backorders' => $object->get_backorders(),
'backorders_allowed' => $object->backorders_allowed(),
'backordered' => $object->is_on_backorder(),
'low_stock_amount' => '' === $object->get_low_stock_amount() ? null : $object->get_low_stock_amount(),
'weight' => $object->get_weight(),
'dimensions' => array(
'length' => $object->get_length(),
@ -185,9 +186,18 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
$variation->set_stock_quantity( $stock_quantity );
}
// isset() returns false for value null, thus we need to check whether the value has been sent by the request.
if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) {
if ( null === $request['low_stock_amount'] ) {
$variation->set_low_stock_amount( '' );
} else {
$variation->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) );
}
}
} else {
$variation->set_backorders( 'no' );
$variation->set_stock_quantity( '' );
$variation->set_low_stock_amount( '' );
}
// Regular Price.
@ -597,6 +607,11 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'low_stock_amount' => array(
'description' => __( 'Low Stock amount for the variation.', 'woocommerce' ),
'type' => array( 'integer', 'null' ),
'context' => array( 'view', 'edit' ),
),
'weight' => array(
/* translators: %s: weight unit */
'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ),

View File

@ -551,11 +551,22 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
}
// Low stock amount.
// isset() returns false for value null, thus we need to check whether the value has been sent by the request.
if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) {
if ( null === $request['low_stock_amount'] ) {
$product->set_low_stock_amount( '' );
} else {
$product->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) );
}
}
} else {
// Don't manage stock.
$product->set_manage_stock( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( $stock_status );
$product->set_low_stock_amount( '' );
}
} elseif ( ! $product->is_type( 'variable' ) ) {
$product->set_stock_status( $stock_status );
@ -985,6 +996,11 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'low_stock_amount' => array(
'description' => __( 'Low Stock amount for the product.', 'woocommerce' ),
'type' => array( 'integer', 'null' ),
'context' => array( 'view', 'edit' ),
),
'sold_individually' => array(
'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ),
'type' => 'boolean',

View File

@ -390,15 +390,23 @@ 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.3.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 ) {
if ( $product->is_type( 'variation' ) ) {
$product = wc_get_product( $product->get_parent_id() );
}
$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 );
}

View File

@ -6,6 +6,9 @@
* @since 3.5.0
*/
/**
* Product_Variations_API class.
*/
class Product_Variations_API extends WC_REST_Unit_Test_Case {
/**
@ -397,7 +400,7 @@ class Product_Variations_API extends WC_REST_Unit_Test_Case {
$data = $response->get_data();
$properties = $data['schema']['properties'];
$this->assertEquals( 37, count( $properties ) );
$this->assertEquals( 38, count( $properties ) );
$this->assertArrayHasKey( 'id', $properties );
$this->assertArrayHasKey( 'date_created', $properties );
$this->assertArrayHasKey( 'date_modified', $properties );
@ -424,6 +427,7 @@ class Product_Variations_API extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'backorders', $properties );
$this->assertArrayHasKey( 'backorders_allowed', $properties );
$this->assertArrayHasKey( 'backordered', $properties );
$this->assertArrayHasKey( 'low_stock_amount', $properties );
$this->assertArrayHasKey( 'weight', $properties );
$this->assertArrayHasKey( 'dimensions', $properties );
$this->assertArrayHasKey( 'shipping_class', $properties );

View File

@ -642,7 +642,7 @@ class WC_Tests_API_Product extends WC_REST_Unit_Test_Case {
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$properties = $data['schema']['properties'];
$this->assertEquals( 65, count( $properties ) );
$this->assertEquals( 66, count( $properties ) );
}
/**

View File

@ -65,6 +65,7 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
'backorders',
'backorders_allowed',
'backordered',
'low_stock_amount',
'sold_individually',
'weight',
'dimensions',

View File

@ -191,4 +191,159 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
}
}
}
/**
* Test wc_get_low_stock_amount with a simple product which has low stock amount set.
*/
public function test_wc_get_low_stock_amount_simple_set() {
$product_low_stock_amount = 5;
$site_wide_low_stock_amount = 3;
// Set the store-wide default.
update_option( 'woocommerce_notify_low_stock_amount', $site_wide_low_stock_amount );
// Simple product, set low stock amount.
$product = WC_Helper_Product::create_simple_product(
true,
array(
'manage_stock' => true,
'stock_quantity' => 10,
'low_stock_amount' => $product_low_stock_amount,
)
);
$this->assertEquals( $product_low_stock_amount, wc_get_low_stock_amount( $product ) );
}
/**
* Test wc_get_low_stock_amount with a simple product which doesn't have low stock amount set.
*/
public function test_wc_get_low_stock_amount_simple_unset() {
$site_wide_low_stock_amount = 3;
// Set the store-wide default.
update_option( 'woocommerce_notify_low_stock_amount', $site_wide_low_stock_amount );
// Simple product, don't set low stock amount.
$product = WC_Helper_Product::create_simple_product(
true,
array(
'manage_stock' => true,
'stock_quantity' => 10,
)
);
$this->assertEquals( $site_wide_low_stock_amount, wc_get_low_stock_amount( $product ) );
}
/**
* Test wc_get_low_stock_amount with a variable product which has low stock amount set on the variation level,
* but not on the parent level. Should use the value from the variation.
*/
public function test_wc_get_low_stock_amount_variation_set_parent_unset() {
$site_wide_low_stock_amount = 3;
$variation_low_stock_amount = 7;
// Set the store-wide default.
update_option( 'woocommerce_notify_low_stock_amount', $site_wide_low_stock_amount );
// Parent low stock amount NOT set.
$variable_product = WC_Helper_Product::create_variation_product();
$variable_product->set_manage_stock( false );
$variable_product->save();
// Set the variation low stock amount.
$variations = $variable_product->get_available_variations( 'objects' );
$var1 = $variations[0];
$var1->set_manage_stock( true );
$var1->set_low_stock_amount( $variation_low_stock_amount );
$var1->save();
$this->assertEquals( $variation_low_stock_amount, wc_get_low_stock_amount( $var1 ) );
// Even after turning on manage stock on the parent, but with no value.
$variable_product->set_manage_stock( true );
$variable_product->save();
$this->assertEquals( $variation_low_stock_amount, wc_get_low_stock_amount( $var1 ) );
// Ans also after turning the manage stock off again on the parent.
$variable_product->set_manage_stock( false );
$variable_product->save();
$this->assertEquals( $variation_low_stock_amount, wc_get_low_stock_amount( $var1 ) );
}
/**
* Test wc_get_low_stock_amount with a variable product which has low stock amount set on the variation level,
* and also on the parent level. Should use the value from the variation.
*/
public function test_wc_get_low_stock_amount_variation_set_parent_set() {
$site_wide_low_stock_amount = 3;
$parent_low_stock_amount = 5;
$variation_low_stock_amount = 7;
// Set the store-wide default.
update_option( 'woocommerce_notify_low_stock_amount', $site_wide_low_stock_amount );
// Set the parent low stock amount.
$variable_product = WC_Helper_Product::create_variation_product();
$variable_product->set_manage_stock( true );
$variable_product->set_low_stock_amount( $parent_low_stock_amount );
$variable_product->save();
// Set the variation low stock amount.
$variations = $variable_product->get_available_variations( 'objects' );
$var1 = $variations[0];
$var1->set_manage_stock( true );
$var1->set_low_stock_amount( $variation_low_stock_amount );
$var1->save();
$this->assertEquals( $variation_low_stock_amount, wc_get_low_stock_amount( $var1 ) );
}
/**
* Test wc_get_low_stock_amount with a variable product which has low stock amount set on the parent level,
* but NOT on the variation level. Should use the value from the parent.
*/
public function test_wc_get_low_stock_amount_variation_unset_parent_set() {
$site_wide_low_stock_amount = 3;
$parent_low_stock_amount = 5;
// Set the store-wide default.
update_option( 'woocommerce_notify_low_stock_amount', $site_wide_low_stock_amount );
// Set the parent low stock amount.
$variable_product = WC_Helper_Product::create_variation_product();
$variable_product->set_manage_stock( true );
$variable_product->set_low_stock_amount( $parent_low_stock_amount );
$variable_product->save();
// Don't set the variation low stock amount.
$variations = $variable_product->get_available_variations( 'objects' );
$var1 = $variations[0];
$this->assertEquals( $parent_low_stock_amount, wc_get_low_stock_amount( $var1 ) );
}
/**
* Test wc_get_low_stock_amount with a variable product which *doesn't have* low stock amount set either on the parent level,
* or on the variation level. Should use the value from the site-wide setting.
*/
public function test_wc_get_low_stock_amount_variation_unset_parent_unset() {
$site_wide_low_stock_amount = 3;
// Set the store-wide default.
update_option( 'woocommerce_notify_low_stock_amount', $site_wide_low_stock_amount );
// Set the parent low stock amount.
$variable_product = WC_Helper_Product::create_variation_product();
$variable_product->set_manage_stock( false );
// Don't set the variation low stock amount.
$variations = $variable_product->get_available_variations( 'objects' );
$var1 = $variations[0];
$var1->set_manage_stock( false );
$this->assertEquals( $site_wide_low_stock_amount, wc_get_low_stock_amount( $var1 ) );
}
}