diff --git a/plugins/woocommerce-admin/client/analytics/report/products/table-variations.js b/plugins/woocommerce-admin/client/analytics/report/products/table-variations.js index d2ad073a2da..2f75838b081 100644 --- a/plugins/woocommerce-admin/client/analytics/report/products/table-variations.js +++ b/plugins/woocommerce-admin/client/analytics/report/products/table-variations.js @@ -2,7 +2,7 @@ /** * External dependencies */ -import { __, _n } from '@wordpress/i18n'; +import { __, _n, _x } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; import { map, get } from 'lodash'; @@ -18,6 +18,7 @@ import { getNewPath, getPersistedQuery } from '@woocommerce/navigation'; */ import ReportTable from 'analytics/components/report-table'; import { numberFormat } from 'lib/number'; +import { isLowStock } from './utils'; export default class VariationsReportTable extends Component { constructor() { @@ -27,13 +28,6 @@ export default class VariationsReportTable extends Component { this.getRowsContent = this.getRowsContent.bind( this ); } - getVariationName( row ) { - const extendedInfo = get( row, 'extended_info', {} ); - const attributes = get( extendedInfo, 'attributes', {} ); - - return extendedInfo.name + ' / ' + attributes.map( a => a.option ).join( ', ' ); - } - getHeadersContent() { return [ { @@ -82,15 +76,9 @@ export default class VariationsReportTable extends Component { const persistedQuery = getPersistedQuery( query ); return map( data, row => { - const { - items_sold, - gross_revenue, - orders_count, - stock_status = 'outofstock', - stock_quantity = '0', - product_id, - } = row; - const name = this.getVariationName( row ); + const { items_sold, gross_revenue, orders_count, extended_info, product_id } = row; + const { stock_status, stock_quantity, low_stock_amount } = extended_info; + const name = get( row, [ 'extended_info', 'name' ], '' ).replace( ' - ', ' / ' ); const ordersLink = getNewPath( persistedQuery, 'orders', { filter: 'advanced', product_includes: query.products, @@ -123,10 +111,12 @@ export default class VariationsReportTable extends Component { value: orders_count, }, { - display: ( + display: isLowStock( stock_status, stock_quantity, low_stock_amount ) ? ( - { stockStatuses[ stock_status ] } + { _x( 'Low', 'Indication of a low quantity', 'wc-admin' ) } + ) : ( + stockStatuses[ stock_status ] ), value: stockStatuses[ stock_status ], }, diff --git a/plugins/woocommerce-admin/client/analytics/report/products/table.js b/plugins/woocommerce-admin/client/analytics/report/products/table.js index 9f2f7fffb4c..7ccbc2d84b0 100644 --- a/plugins/woocommerce-admin/client/analytics/report/products/table.js +++ b/plugins/woocommerce-admin/client/analytics/report/products/table.js @@ -2,7 +2,7 @@ /** * External dependencies */ -import { __, _n } from '@wordpress/i18n'; +import { __, _n, _x } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; import { map } from 'lodash'; @@ -18,6 +18,7 @@ import { getNewPath, getPersistedQuery } from '@woocommerce/navigation'; */ import ReportTable from 'analytics/components/report-table'; import { numberFormat } from 'lib/number'; +import { isLowStock } from './utils'; export default class ProductsReportTable extends Component { constructor() { @@ -97,10 +98,8 @@ export default class ProductsReportTable extends Component { orders_count, categories = [], // @TODO variations = [], // @TODO - stock_status = 'outofstock', // @TODO - stock_quantity = '0', // @TODO } = row; - const { name } = extended_info; + const { name, stock_status, stock_quantity, low_stock_amount } = extended_info; const ordersLink = getNewPath( persistedQuery, 'orders', { filter: 'advanced', product_includes: product_id, @@ -150,10 +149,12 @@ export default class ProductsReportTable extends Component { value: variations.length, }, { - display: ( + display: isLowStock( stock_status, stock_quantity, low_stock_amount ) ? ( - { stockStatuses[ stock_status ] } + { _x( 'Low', 'Indication of a low quantity', 'wc-admin' ) } + ) : ( + stockStatuses[ stock_status ] ), value: stockStatuses[ stock_status ], }, diff --git a/plugins/woocommerce-admin/client/analytics/report/products/utils.js b/plugins/woocommerce-admin/client/analytics/report/products/utils.js new file mode 100644 index 00000000000..e966b61b240 --- /dev/null +++ b/plugins/woocommerce-admin/client/analytics/report/products/utils.js @@ -0,0 +1,15 @@ +/** + * Determine if a product or variation is in low stock. + * + * @format + * @param {number} threshold - The number at which stock is determined to be low. + * @returns {boolean} - Whether or not the stock is low. + */ + +export function isLowStock( status, quantity, threshold ) { + if ( ! quantity ) { + // Sites that don't do inventory tracking will always return false. + return false; + } + return 'instock' === status && quantity <= threshold; +} diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-products-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-products-controller.php index e51d663b24c..ff348c10c78 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-products-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-products-controller.php @@ -175,6 +175,56 @@ class WC_Admin_REST_Reports_Products_Controller extends WC_REST_Reports_Controll 'context' => array( 'view', 'edit' ), 'description' => __( 'Number of orders product appeared in.', 'wc-admin' ), ), + 'extended_info' => array( + 'name' => array( + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product name.', 'wc-admin' ), + ), + 'price' => array( + 'type' => 'number', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product price.', 'wc-admin' ), + ), + 'image' => array( + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product image.', 'wc-admin' ), + ), + 'permalink' => array( + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product link.', 'wc-admin' ), + ), + 'attributes' => array( + 'type' => 'array', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product attributes.', 'wc-admin' ), + ), + 'stock_status' => array( + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product inventory status.', 'wc-admin' ), + ), + 'stock_quantity' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product inventory quantity.', 'wc-admin' ), + ), + 'low_stock_amount' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product inventory threshold for low stock.', 'wc-admin' ), + ), + ), ), ); diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-variations-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-variations-controller.php index 2ecd099b2c3..4091a3d604d 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-variations-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-variations-controller.php @@ -31,6 +31,15 @@ class WC_Admin_REST_Reports_Variations_Controller extends WC_REST_Reports_Contro */ protected $rest_base = 'reports/variations'; + /** + * Mapping between external parameter name and name used in query class. + * + * @var array + */ + protected $param_mapping = array( + 'products' => 'product_includes', + ); + /** * Get items. * @@ -43,7 +52,11 @@ class WC_Admin_REST_Reports_Variations_Controller extends WC_REST_Reports_Contro $registered = array_keys( $this->get_collection_params() ); foreach ( $registered as $param_name ) { if ( isset( $request[ $param_name ] ) ) { - $args[ $param_name ] = $request[ $param_name ]; + if ( isset( $this->param_mapping[ $param_name ] ) ) { + $args[ $this->param_mapping[ $param_name ] ] = $request[ $param_name ]; + } else { + $args[ $param_name ] = $request[ $param_name ]; + } } } @@ -202,6 +215,24 @@ class WC_Admin_REST_Reports_Variations_Controller extends WC_REST_Reports_Contro 'context' => array( 'view', 'edit' ), 'description' => __( 'Product attributes.', 'wc-admin' ), ), + 'stock_status' => array( + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product inventory status.', 'wc-admin' ), + ), + 'stock_quantity' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product inventory quantity.', 'wc-admin' ), + ), + 'low_stock_amount' => array( + 'type' => 'integer', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Product inventory threshold for low stock.', 'wc-admin' ), + ), ), ), ); diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php index e8bb1bd3c90..76c97dec01a 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php @@ -60,6 +60,9 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i 'price', 'image', 'permalink', + 'stock_status', + 'stock_quantity', + 'low_stock_amount', ); /** @@ -154,6 +157,10 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i $extended_info[ $extended_attribute ] = $value; } } + // If there is no set low_stock_amount, use the one in user settings. + if ( '' === $extended_info['low_stock_amount'] ) { + $extended_info['low_stock_amount'] = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); + } $extended_info = $this->cast_numbers( $extended_info ); } $products_data[ $key ]['extended_info'] = $extended_info; diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-variations-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-variations-data-store.php index eae58846b6d..13d0b198648 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-variations-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-variations-data-store.php @@ -61,6 +61,9 @@ class WC_Admin_Reports_Variations_Data_Store extends WC_Admin_Reports_Data_Store 'price', 'image', 'permalink', + 'stock_status', + 'stock_quantity', + 'low_stock_amount', ); @@ -123,20 +126,23 @@ class WC_Admin_Reports_Variations_Data_Store extends WC_Admin_Reports_Data_Store foreach ( $products_data as $key => $product_data ) { $extended_info = new ArrayObject(); if ( $query_args['extended_info'] ) { + $extended_attributes = apply_filters( 'woocommerce_rest_reports_variations_extended_attributes', $this->extended_attributes, $product_data ); $product = wc_get_product( $product_data['product_id'] ); - $extended_attributes = apply_filters( 'woocommerce_rest_reports_products_extended_attributes', $this->extended_attributes, $product_data ); - foreach ( $extended_attributes as $extended_attribute ) { - $function = 'get_' . $extended_attribute; - if ( is_callable( array( $product, $function ) ) && 'get_price' !== $function ) { - $value = $product->{$function}(); - $extended_info[ $extended_attribute ] = $value; - } + $variations = array(); + if ( method_exists( $product, 'get_available_variations' ) ) { + $variations = $product->get_available_variations(); } - $variations = $product->get_available_variations(); foreach ( $variations as $variation ) { if ( (int) $variation['variation_id'] === (int) $product_data['variation_id'] ) { $attributes = array(); $variation_product = wc_get_product( $variation['variation_id'] ); + foreach ( $extended_attributes as $extended_attribute ) { + $function = 'get_' . $extended_attribute; + if ( is_callable( array( $variation_product, $function ) ) ) { + $value = $variation_product->{$function}(); + $extended_info[ $extended_attribute ] = $value; + } + } foreach ( $variation['attributes'] as $attribute_name => $attribute ) { $name = str_replace( 'attribute_', '', $attribute_name ); $option_term = get_term_by( 'slug', $attribute, $name ); @@ -147,9 +153,12 @@ class WC_Admin_Reports_Variations_Data_Store extends WC_Admin_Reports_Data_Store ); } $extended_info['attributes'] = $attributes; - $extended_info['price'] = $variation_product->get_price(); } } + // If there is no set low_stock_amount, use the one in user settings. + if ( '' === $extended_info['low_stock_amount'] ) { + $extended_info['low_stock_amount'] = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); + } $extended_info = $this->cast_numbers( $extended_info ); } $products_data[ $key ]['extended_info'] = $extended_info; diff --git a/plugins/woocommerce-admin/tests/api/reports-products.php b/plugins/woocommerce-admin/tests/api/reports-products.php index a946650c22a..92d2f69e98f 100644 --- a/plugins/woocommerce-admin/tests/api/reports-products.php +++ b/plugins/woocommerce-admin/tests/api/reports-products.php @@ -5,6 +5,13 @@ * @package WooCommerce\Tests\API * @since 3.5.0 */ + +/** + * Reports Products REST API Test Class + * + * @package WooCommerce\Tests\API + * @since 3.5.0 + */ class WC_Tests_API_Reports_Products extends WC_REST_Unit_Test_Case { /** @@ -99,10 +106,11 @@ class WC_Tests_API_Reports_Products extends WC_REST_Unit_Test_Case { $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertEquals( 4, count( $properties ) ); + $this->assertEquals( 5, count( $properties ) ); $this->assertArrayHasKey( 'product_id', $properties ); $this->assertArrayHasKey( 'items_sold', $properties ); $this->assertArrayHasKey( 'gross_revenue', $properties ); $this->assertArrayHasKey( 'orders_count', $properties ); + $this->assertArrayHasKey( 'extended_info', $properties ); } } diff --git a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-products.php b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-products.php index b82e8d06085..d213b444c4a 100644 --- a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-products.php +++ b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-products.php @@ -1,11 +1,17 @@ assertEquals( $expected_data, $query->get_data() ); } + /** + * Test the ordering of results by product name + * + * @since 3.5.0 + */ public function test_order_by_product_name() { WC_Helper_Reports::reset_stats_dbs(); @@ -183,7 +194,11 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case { $product = new WC_Product_Simple(); $product->set_name( 'Test Product' ); $product->set_regular_price( 25 ); + $product->set_manage_stock( true ); + $product->set_stock_quantity( 25 ); + $product->set_low_stock_amount( 5 ); $product->save(); + $order = WC_Helper_Order::create_order( 1, $product ); $order->set_status( 'completed' ); $order->set_shipping_total( 10 ); @@ -214,10 +229,13 @@ class WC_Tests_Reports_Products extends WC_Unit_Test_Case { 'gross_revenue' => 100.0, // $25 * 4. 'orders_count' => 1, 'extended_info' => array( - 'name' => $product->get_name(), - 'image' => $product->get_image(), - 'permalink' => $product->get_permalink(), - 'price' => (float) $product->get_price(), + 'name' => $product->get_name(), + 'image' => $product->get_image(), + 'permalink' => $product->get_permalink(), + 'price' => (float) $product->get_price(), + 'stock_status' => $product->get_stock_status(), + 'stock_quantity' => $product->get_stock_quantity() - 4, // subtract the ones purchased. + 'low_stock_amount' => $product->get_low_stock_amount(), ), ), ), diff --git a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-variations.php b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-variations.php index bba18f73fea..89075119860 100644 --- a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-variations.php +++ b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-variations.php @@ -1,11 +1,17 @@ set_parent_id( $product->get_id() ); $variation->set_regular_price( 10 ); $variation->set_attributes( array( 'pa_color' => 'green' ) ); + $variation->set_manage_stock( true ); + $variation->set_stock_quantity( 25 ); $variation->save(); $order = WC_Helper_Order::create_order( 1, $variation ); @@ -120,11 +128,14 @@ class WC_Tests_Reports_Variations extends WC_Unit_Test_Case { 'gross_revenue' => 40.0, // $10 * 4. 'orders_count' => 1, 'extended_info' => array( - 'name' => $product->get_name(), - 'image' => $variation->get_image(), - 'permalink' => $product->get_permalink(), - 'price' => (float) $variation->get_price(), - 'attributes' => array( + 'name' => $variation->get_name(), + 'image' => $variation->get_image(), + 'permalink' => $variation->get_permalink(), + 'price' => (float) $variation->get_price(), + 'stock_status' => $variation->get_stock_status(), + 'stock_quantity' => $variation->get_stock_quantity() - 4, // subtract the ones purchased. + 'low_stock_amount' => get_option( 'woocommerce_notify_low_stock_amount' ), + 'attributes' => array( 0 => array( 'id' => 0, 'name' => 'color',