Match server-side CSV export format to client-side (https://github.com/woocommerce/woocommerce-admin/pull/2987)
* Add "exportable" report interface for defining CSV export values. * Define export values for Orders Report. * Define export values for Products Report. * Define export values for Categories Report. * Define export values for Coupons Report. * Allow commas and double quotes in CSV exported values. * Fix in-browser export formatting of orders report products. * Align server-side orders report export formatting with in-browser. * Cover comma and double quote escaping in CSV export package tests. * Define export values for Customers Report. * Embed response links when requesting data for CSV exports. * Define export values for Downloads Report. * Move reusable report export functions to a trait. * Define export values for Stock Report. * Define export values for Taxes Report. * Define export values for Variations Report. * Define export values for Revenue Report. * Always pass export row data through the filter. * Fix formatting in test case for CSV coupon export. * Quote escape CSV headers in client-side export. Escape values with spaces as well. * Check if inventory is managed at the product level before using the stock status/quantity. * Prevent CSV injection in csv-export package.
This commit is contained in:
parent
dd77d25a34
commit
1ac8577fc2
|
@ -186,7 +186,11 @@ export default class OrdersReportTable extends Component {
|
|||
href: product.href,
|
||||
} ) )
|
||||
),
|
||||
value: formattedProducts.map( product => product.label ).join( ' ' ),
|
||||
value: formattedProducts
|
||||
.map( ( { quantity, label } ) =>
|
||||
sprintf( __( '%s× %s', 'woocommerce-admin' ), quantity, label )
|
||||
)
|
||||
.join( ', ' ),
|
||||
},
|
||||
{
|
||||
display: numberFormat( num_items_sold ),
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
# 1.2.0 (Unreleased)
|
||||
|
||||
- Properly escape values with double quotes.
|
||||
- Prevent CSV injection.
|
||||
|
||||
# 1.1.2
|
||||
|
||||
- Update dependencies.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/** @format */
|
||||
|
||||
export default `Date,Orders,Description,Gross Revenue,Refunds,Coupons,Taxes,Shipping,Net Revenue
|
||||
2018-04-29T00:00:00,30,lorem ipsum,200,19,19,100,19,200`;
|
||||
export default `Date,Orders,Description,"Gross Revenue",Refunds,Coupons,Taxes,Shipping,"Net Revenue","Negative Number"
|
||||
2018-04-29T00:00:00,30,"Lorem, ""ipsum""",200,19,19,100,19,200,'-123`;
|
||||
|
|
|
@ -37,4 +37,8 @@ export default [
|
|||
label: 'Net Revenue',
|
||||
key: 'net_revenue',
|
||||
},
|
||||
{
|
||||
label: 'Negative Number',
|
||||
key: 'neg_num',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -11,8 +11,8 @@ export default [
|
|||
value: '30',
|
||||
},
|
||||
{
|
||||
display: 'Lorem, ipsum',
|
||||
value: 'lorem, ipsum',
|
||||
display: 'Lorem, "ipsum"',
|
||||
value: 'Lorem, "ipsum"',
|
||||
},
|
||||
{
|
||||
display: '€200.00',
|
||||
|
@ -38,5 +38,9 @@ export default [
|
|||
display: '€200.00',
|
||||
value: 200,
|
||||
},
|
||||
{
|
||||
display: '-123',
|
||||
value: -123,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
|
|
@ -5,17 +5,40 @@
|
|||
import moment from 'moment';
|
||||
import { saveAs } from 'browser-filesaver';
|
||||
|
||||
function escapeCSVValue( value ) {
|
||||
let stringValue = value.toString();
|
||||
|
||||
// Prevent CSV injection.
|
||||
// See: http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/
|
||||
// See: WC_CSV_Exporter::escape_data()
|
||||
if ( [ '=', '+', '-', '@' ].includes( stringValue.charAt( 0 ) ) ) {
|
||||
stringValue = "'" + stringValue;
|
||||
} else if ( stringValue.match( /[,"\s]/ ) ) {
|
||||
stringValue = '"' + stringValue.replace( /"/g, '""' ) + '"';
|
||||
}
|
||||
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
function getCSVHeaders( headers ) {
|
||||
return Array.isArray( headers ) ? headers.map( header => header.label ).join( ',' ) : [];
|
||||
return Array.isArray( headers )
|
||||
? headers
|
||||
.map( header => escapeCSVValue( header.label ) )
|
||||
.join( ',' )
|
||||
: [];
|
||||
}
|
||||
|
||||
function getCSVRows( rows ) {
|
||||
return Array.isArray( rows )
|
||||
? rows
|
||||
.map( row =>
|
||||
row.map( rowItem =>
|
||||
rowItem.value !== undefined && rowItem.value !== null ? rowItem.value.toString().replace( /,/g, ''
|
||||
) : '' ).join( ',' )
|
||||
row.map( rowItem => {
|
||||
if ( undefined === rowItem.value || null === rowItem.value ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return escapeCSVValue( rowItem.value );
|
||||
} ).join( ',' )
|
||||
)
|
||||
.join( '\n' )
|
||||
: [];
|
||||
|
|
|
@ -11,13 +11,16 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Categories;
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
|
||||
|
||||
/**
|
||||
* REST API Reports categories controller class.
|
||||
*
|
||||
* @package WooCommerce/API
|
||||
* @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
|
||||
*/
|
||||
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
||||
class Controller extends ReportsController implements ExportableInterface {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
|
@ -318,4 +321,35 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column names for export.
|
||||
*
|
||||
* @return array Key value pair of Column ID => Label.
|
||||
*/
|
||||
public function get_export_columns() {
|
||||
return array(
|
||||
'category' => __( 'Category', 'woocommerce-admin' ),
|
||||
'items_sold' => __( 'Items Sold', 'woocommerce-admin' ),
|
||||
'net_revenue' => __( 'Net Revenue', 'woocommerce-admin' ),
|
||||
'products_count' => __( 'Products', 'woocommerce-admin' ),
|
||||
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column values for export.
|
||||
*
|
||||
* @param array $item Single report item/row.
|
||||
* @return array Key value pair of Column ID => Row Value.
|
||||
*/
|
||||
public function prepare_item_for_export( $item ) {
|
||||
return array(
|
||||
'category' => $item['extended_info']['name'],
|
||||
'items_sold' => $item['items_sold'],
|
||||
'net_revenue' => $item['net_revenue'],
|
||||
'products_count' => $item['products_count'],
|
||||
'orders_count' => $item['orders_count'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -294,10 +294,20 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
* @return array
|
||||
*/
|
||||
public function get_order_statuses() {
|
||||
return array_keys( $this->get_order_status_labels() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order statuses (and labels) without prefixes.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_order_status_labels() {
|
||||
$order_statuses = array();
|
||||
|
||||
foreach ( array_keys( wc_get_order_statuses() ) as $status ) {
|
||||
$order_statuses[] = str_replace( 'wc-', '', $status );
|
||||
foreach ( wc_get_order_statuses() as $key => $label ) {
|
||||
$new_key = str_replace( 'wc-', '', $key );
|
||||
$order_statuses[ $new_key ] = $label;
|
||||
}
|
||||
|
||||
return $order_statuses;
|
||||
|
|
|
@ -11,13 +11,15 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Coupons;
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
|
||||
|
||||
/**
|
||||
* REST API Reports coupons controller class.
|
||||
*
|
||||
* @package WooCommerce/API
|
||||
* @extends WC_REST_Reports_Controller
|
||||
*/
|
||||
class Controller extends \WC_REST_Reports_Controller {
|
||||
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
|
@ -289,4 +291,41 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column names for export.
|
||||
*
|
||||
* @return array Key value pair of Column ID => Label.
|
||||
*/
|
||||
public function get_export_columns() {
|
||||
return array(
|
||||
'code' => __( 'Coupon Code', 'woocommerce-admin' ),
|
||||
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
|
||||
'amount' => __( 'Amount Discounted', 'woocommerce-admin' ),
|
||||
'created' => __( 'Created', 'woocommerce-admin' ),
|
||||
'expires' => __( 'Expires', 'woocommerce-admin' ),
|
||||
'type' => __( 'Type', 'woocommerce-admin' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column values for export.
|
||||
*
|
||||
* @param array $item Single report item/row.
|
||||
* @return array Key value pair of Column ID => Row Value.
|
||||
*/
|
||||
public function prepare_item_for_export( $item ) {
|
||||
$date_expires = empty( $item['extended_info']['date_expires'] )
|
||||
? __( 'N/A', 'woocommerce-admin' )
|
||||
: $item['extended_info']['date_expires'];
|
||||
|
||||
return array(
|
||||
'code' => $item['extended_info']['code'],
|
||||
'orders_count' => $item['orders_count'],
|
||||
'amount' => $item['amount'],
|
||||
'created' => $item['extended_info']['date_created'],
|
||||
'expires' => $date_expires,
|
||||
'type' => $item['extended_info']['discount_type'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Customers;
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
|
||||
|
||||
/**
|
||||
|
@ -19,7 +21,11 @@ use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
|
|||
* @package WooCommerce/API
|
||||
* @extends WC_REST_Reports_Controller
|
||||
*/
|
||||
class Controller extends \WC_REST_Reports_Controller {
|
||||
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
|
||||
/**
|
||||
* Exportable traits.
|
||||
*/
|
||||
use ExportableTraits;
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
|
@ -221,7 +227,7 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'state' => array(
|
||||
'state' => array(
|
||||
'description' => __( 'Region.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
|
@ -525,4 +531,49 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column names for export.
|
||||
*
|
||||
* @return array Key value pair of Column ID => Label.
|
||||
*/
|
||||
public function get_export_columns() {
|
||||
return array(
|
||||
'name' => __( 'Name', 'woocommerce-admin' ),
|
||||
'username' => __( 'Username', 'woocommerce-admin' ),
|
||||
'last_active' => __( 'Last Active', 'woocommerce-admin' ),
|
||||
'registered' => __( 'Sign Up', 'woocommerce-admin' ),
|
||||
'email' => __( 'Email', 'woocommerce-admin' ),
|
||||
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
|
||||
'total_spend' => __( 'Total Spend', 'woocommerce-admin' ),
|
||||
'avg_order_value' => __( 'AOV', 'woocommerce-admin' ),
|
||||
'country' => __( 'Country', 'woocommerce-admin' ),
|
||||
'city' => __( 'City', 'woocommerce-admin' ),
|
||||
'region' => __( 'Region', 'woocommerce-admin' ),
|
||||
'postcode' => __( 'Postal Code', 'woocommerce-admin' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column values for export.
|
||||
*
|
||||
* @param array $item Single report item/row.
|
||||
* @return array Key value pair of Column ID => Row Value.
|
||||
*/
|
||||
public function prepare_item_for_export( $item ) {
|
||||
return array(
|
||||
'name' => $item['name'],
|
||||
'username' => $item['username'],
|
||||
'last_active' => $item['date_last_active'],
|
||||
'registered' => $item['date_registered'],
|
||||
'email' => $item['email'],
|
||||
'orders_count' => $item['orders_count'],
|
||||
'total_spend' => self::csv_number_format( $item['total_spend'] ),
|
||||
'avg_order_value' => self::csv_number_format( $item['avg_order_value'] ),
|
||||
'country' => $item['country'],
|
||||
'city' => $item['city'],
|
||||
'region' => $item['state'],
|
||||
'postcode' => $item['postcode'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,13 +11,16 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Downloads;
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
|
||||
|
||||
/**
|
||||
* REST API Reports downloads controller class.
|
||||
*
|
||||
* @package WooCommerce/API
|
||||
* @extends Automattic\WooCommerce\Admin\API\Reports\Controller
|
||||
*/
|
||||
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
||||
class Controller extends ReportsController implements ExportableInterface {
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
|
@ -378,4 +381,37 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column names for export.
|
||||
*
|
||||
* @return array Key value pair of Column ID => Label.
|
||||
*/
|
||||
public function get_export_columns() {
|
||||
return array(
|
||||
'date' => __( 'Date', 'woocommerce-admin' ),
|
||||
'product' => __( 'Product Title', 'woocommerce-admin' ),
|
||||
'file_name' => __( 'File Name', 'woocommerce-admin' ),
|
||||
'order_number' => __( 'Order #', 'woocommerce-admin' ),
|
||||
'user_id' => __( 'User Name', 'woocommerce-admin' ),
|
||||
'ip_address' => __( 'IP', 'woocommerce-admin' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column values for export.
|
||||
*
|
||||
* @param array $item Single report item/row.
|
||||
* @return array Key value pair of Column ID => Row Value.
|
||||
*/
|
||||
public function prepare_item_for_export( $item ) {
|
||||
return array(
|
||||
'date' => $item['date'],
|
||||
'product' => $item['_embedded']['product'][0]['name'],
|
||||
'file_name' => $item['file_name'],
|
||||
'order_number' => $item['order_number'],
|
||||
'user_id' => $item['username'],
|
||||
'ip_address' => $item['ip_address'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
/**
|
||||
* Reports Exportable Controller Interface
|
||||
*
|
||||
* @package WooCommerce Admin/Interface
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\API\Reports;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* WooCommerce Reports exportable controller interface.
|
||||
*
|
||||
* @since 3.5.0
|
||||
*/
|
||||
interface ExportableInterface {
|
||||
|
||||
/**
|
||||
* Get the column names for export.
|
||||
*
|
||||
* @return array Key value pair of Column ID => Label.
|
||||
*/
|
||||
public function get_export_columns();
|
||||
|
||||
/**
|
||||
* Get the column values for export.
|
||||
*
|
||||
* @param array $item Single report item/row.
|
||||
* @return array Key value pair of Column ID => Value.
|
||||
*/
|
||||
public function prepare_item_for_export( $item );
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
/**
|
||||
* REST API Reports exportable traits
|
||||
*
|
||||
* Collection of utility methods for exportable reports.
|
||||
*
|
||||
* @package WooCommerce Admin/API
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\API\Reports;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* ExportableTraits class.
|
||||
*/
|
||||
trait ExportableTraits {
|
||||
/**
|
||||
* Format numbers for CSV using store precision setting.
|
||||
*
|
||||
* @param string|float $value Numeric value.
|
||||
* @return string Formatted value.
|
||||
*/
|
||||
public static function csv_number_format( $value ) {
|
||||
$decimals = wc_get_price_decimals();
|
||||
// See: @woocommerce/currency: getCurrencyFormatDecimal().
|
||||
return number_format( $value, $decimals, '.', '' );
|
||||
}
|
||||
}
|
|
@ -11,13 +11,16 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Orders;
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
|
||||
|
||||
/**
|
||||
* REST API Reports orders controller class.
|
||||
*
|
||||
* @package WooCommerce/API
|
||||
* @extends \Automattic\WooCommerce\Admin\API\Reports\Controller
|
||||
*/
|
||||
class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
||||
class Controller extends ReportsController implements ExportableInterface {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
|
@ -410,4 +413,72 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get products column export value.
|
||||
*
|
||||
* @param array $products Products from report row.
|
||||
* @return string
|
||||
*/
|
||||
protected function _get_products( $products ) {
|
||||
$products_list = array();
|
||||
|
||||
foreach ( $products as $product ) {
|
||||
$products_list[] = sprintf(
|
||||
/* translators: 1: numeric product quantity, 2: name of product */
|
||||
__( '%1$s× %2$s', 'woocommerce-admin' ),
|
||||
$product['quantity'],
|
||||
$product['name']
|
||||
);
|
||||
}
|
||||
|
||||
return implode( ', ', $products_list );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coupons column export value.
|
||||
*
|
||||
* @param array $coupons Coupons from report row.
|
||||
* @return string
|
||||
*/
|
||||
protected function _get_coupons( $coupons ) {
|
||||
return implode( ', ', wp_list_pluck( $coupons, 'code' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column names for export.
|
||||
*
|
||||
* @return array Key value pair of Column ID => Label.
|
||||
*/
|
||||
public function get_export_columns() {
|
||||
return array(
|
||||
'date_created' => __( 'Date', 'woocommerce-admin' ),
|
||||
'order_number' => __( 'Order #', 'woocommerce-admin' ),
|
||||
'status' => __( 'Status', 'woocommerce-admin' ),
|
||||
'customer_type' => __( 'Customer', 'woocommerce-admin' ),
|
||||
'products' => __( 'Product(s)', 'woocommerce-admin' ),
|
||||
'num_items_sold' => __( 'Items Sold', 'woocommerce-admin' ),
|
||||
'coupons' => __( 'Coupon(s)', 'woocommerce-admin' ),
|
||||
'net_total' => __( 'N. Revenue', 'woocommerce-admin' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column values for export.
|
||||
*
|
||||
* @param array $item Single report item/row.
|
||||
* @return array Key value pair of Column ID => Row Value.
|
||||
*/
|
||||
public function prepare_item_for_export( $item ) {
|
||||
return array(
|
||||
'date_created' => $item['date_created'],
|
||||
'order_number' => $item['order_number'],
|
||||
'status' => $item['status'],
|
||||
'customer_type' => $item['customer_type'],
|
||||
'products' => $this->_get_products( $item['extended_info']['products'] ),
|
||||
'num_items_sold' => $item['num_items_sold'],
|
||||
'coupons' => $this->_get_coupons( $item['extended_info']['coupons'] ),
|
||||
'net_total' => $item['net_total'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,13 +11,15 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Products;
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
|
||||
|
||||
/**
|
||||
* REST API Reports products controller class.
|
||||
*
|
||||
* @package WooCommerce/API
|
||||
* @extends WC_REST_Reports_Controller
|
||||
*/
|
||||
class Controller extends \WC_REST_Reports_Controller {
|
||||
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
|
@ -343,4 +345,89 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock status column export value.
|
||||
*
|
||||
* @param array $status Stock status from report row.
|
||||
* @return string
|
||||
*/
|
||||
protected function _get_stock_status( $status ) {
|
||||
$statuses = wc_get_product_stock_status_options();
|
||||
|
||||
return isset( $statuses[ $status ] ) ? $statuses[ $status ] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get categories column export value.
|
||||
*
|
||||
* @param array $category_ids Category IDs from report row.
|
||||
* @return string
|
||||
*/
|
||||
protected function _get_categories( $category_ids ) {
|
||||
$category_names = get_terms(
|
||||
array(
|
||||
'taxonomy' => 'product_cat',
|
||||
'include' => $category_ids,
|
||||
'fields' => 'names',
|
||||
)
|
||||
);
|
||||
|
||||
return implode( ', ', $category_names );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column names for export.
|
||||
*
|
||||
* @return array Key value pair of Column ID => Label.
|
||||
*/
|
||||
public function get_export_columns() {
|
||||
$export_columns = array(
|
||||
'product_name' => __( 'Product Title', 'woocommerce-admin' ),
|
||||
'sku' => __( 'SKU', 'woocommerce-admin' ),
|
||||
'items_sold' => __( 'Items Sold', 'woocommerce-admin' ),
|
||||
'net_revenue' => __( 'N. Revenue', 'woocommerce-admin' ),
|
||||
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
|
||||
'product_cat' => __( 'Category', 'woocommerce-admin' ),
|
||||
'variations' => __( 'Variations', 'woocommerce-admin' ),
|
||||
);
|
||||
|
||||
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
|
||||
$export_columns['stock_status'] = __( 'Status', 'woocommerce-admin' );
|
||||
$export_columns['stock'] = __( 'Stock', 'woocommerce-admin' );
|
||||
}
|
||||
|
||||
return $export_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column values for export.
|
||||
*
|
||||
* @param array $item Single report item/row.
|
||||
* @return array Key value pair of Column ID => Row Value.
|
||||
*/
|
||||
public function prepare_item_for_export( $item ) {
|
||||
$export_item = array(
|
||||
'product_name' => $item['extended_info']['name'],
|
||||
'sku' => $item['extended_info']['sku'],
|
||||
'items_sold' => $item['items_sold'],
|
||||
'net_revenue' => $item['net_revenue'],
|
||||
'orders_count' => $item['orders_count'],
|
||||
'product_cat' => $this->_get_categories( $item['extended_info']['category_ids'] ),
|
||||
'variations' => isset( $item['extended_info']['variations'] ) ? count( $item['extended_info']['variations'] ) : 0,
|
||||
);
|
||||
|
||||
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
|
||||
if ( $item['extended_info']['manage_stock'] ) {
|
||||
$export_item['stock_status'] = $this->_get_stock_status( $item['extended_info']['stock_status'] );
|
||||
$export_item['stock'] = $item['extended_info']['stock_quantity'];
|
||||
} else {
|
||||
$export_item['stock_status'] = __( 'N/A', 'woocommerce-admin' );
|
||||
$export_item['stock'] = __( 'N/A', 'woocommerce-admin' );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $export_item;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Revenue\Stats;
|
|||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Revenue\Query as RevenueQuery;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;
|
||||
|
||||
/**
|
||||
|
@ -20,7 +22,11 @@ use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;
|
|||
* @package WooCommerce/API
|
||||
* @extends WC_REST_Reports_Controller
|
||||
*/
|
||||
class Controller extends \WC_REST_Reports_Controller {
|
||||
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
|
||||
/**
|
||||
* Exportable traits.
|
||||
*/
|
||||
use ExportableTraits;
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
|
@ -60,7 +66,7 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
* Get all reports.
|
||||
*
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return array|WP_Error
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_items( $request ) {
|
||||
$query_args = $this->prepare_reports_query( $request );
|
||||
|
@ -105,6 +111,24 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get report items for export.
|
||||
*
|
||||
* Returns only the interval data.
|
||||
*
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_export_items( $request ) {
|
||||
$response = $this->get_items( $request );
|
||||
$data = $response->get_data();
|
||||
$intervals = $data['intervals'];
|
||||
|
||||
$response->set_data( $intervals );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a report object for serialization.
|
||||
*
|
||||
|
@ -406,4 +430,43 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column names for export.
|
||||
*
|
||||
* @return array Key value pair of Column ID => Label.
|
||||
*/
|
||||
public function get_export_columns() {
|
||||
return array(
|
||||
'date' => __( 'Date', 'woocommerce-admin' ),
|
||||
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
|
||||
'gross_revenue' => __( 'Gross Revenue', 'woocommerce-admin' ),
|
||||
'refunds' => __( 'Refunds', 'woocommerce-admin' ),
|
||||
'coupons' => __( 'Coupons', 'woocommerce-admin' ),
|
||||
'taxes' => __( 'Taxes', 'woocommerce-admin' ),
|
||||
'shipping' => __( 'Shipping', 'woocommerce-admin' ),
|
||||
'net_revenue' => __( 'Net Revenue', 'woocommerce-admin' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column values for export.
|
||||
*
|
||||
* @param array $item Single report item/row.
|
||||
* @return array Key value pair of Column ID => Row Value.
|
||||
*/
|
||||
public function prepare_item_for_export( $item ) {
|
||||
$subtotals = (array) $item['subtotals'];
|
||||
|
||||
return array(
|
||||
'date' => $item['date_start'],
|
||||
'orders_count' => $subtotals['orders_count'],
|
||||
'gross_revenue' => self::csv_number_format( $subtotals['gross_revenue'] ),
|
||||
'refunds' => self::csv_number_format( $subtotals['refunds'] ),
|
||||
'coupons' => self::csv_number_format( $subtotals['coupons'] ),
|
||||
'taxes' => self::csv_number_format( $subtotals['taxes'] ),
|
||||
'shipping' => self::csv_number_format( $subtotals['shipping'] ),
|
||||
'net_revenue' => self::csv_number_format( $subtotals['net_revenue'] ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,13 +11,15 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Stock;
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
|
||||
|
||||
/**
|
||||
* REST API Reports stock controller class.
|
||||
*
|
||||
* @package WooCommerce/API
|
||||
* @extends WC_REST_Reports_Controller
|
||||
*/
|
||||
class Controller extends \WC_REST_Reports_Controller {
|
||||
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
|
@ -514,4 +516,33 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column names for export.
|
||||
*
|
||||
* @return array Key value pair of Column ID => Label.
|
||||
*/
|
||||
public function get_export_columns() {
|
||||
return array(
|
||||
'title' => __( 'Product / Variation', 'woocommerce-admin' ),
|
||||
'sku' => __( 'SKU', 'woocommerce-admin' ),
|
||||
'stock_status' => __( 'Status', 'woocommerce-admin' ),
|
||||
'stock_quantity' => __( 'Stock', 'woocommerce-admin' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column values for export.
|
||||
*
|
||||
* @param array $item Single report item/row.
|
||||
* @return array Key value pair of Column ID => Row Value.
|
||||
*/
|
||||
public function prepare_item_for_export( $item ) {
|
||||
return array(
|
||||
'title' => $item['name'],
|
||||
'sku' => $item['sku'],
|
||||
'stock_status' => $item['stock_status'],
|
||||
'stock_quantity' => $item['stock_quantity'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,13 +11,20 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Taxes;
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
|
||||
|
||||
/**
|
||||
* REST API Reports taxes controller class.
|
||||
*
|
||||
* @package WooCommerce/API
|
||||
* @extends WC_REST_Reports_Controller
|
||||
*/
|
||||
class Controller extends \WC_REST_Reports_Controller {
|
||||
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
|
||||
/**
|
||||
* Exportable traits.
|
||||
*/
|
||||
use ExportableTraits;
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
|
@ -287,4 +294,37 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column names for export.
|
||||
*
|
||||
* @return array Key value pair of Column ID => Label.
|
||||
*/
|
||||
public function get_export_columns() {
|
||||
return array(
|
||||
'tax_code' => __( 'Tax Code', 'woocommerce-admin' ),
|
||||
'rate' => __( 'Rate', 'woocommerce-admin' ),
|
||||
'total_tax' => __( 'Total Tax', 'woocommerce-admin' ),
|
||||
'order_tax' => __( 'Order Tax', 'woocommerce-admin' ),
|
||||
'shipping_tax' => __( 'Shipping Tax', 'woocommerce-admin' ),
|
||||
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column values for export.
|
||||
*
|
||||
* @param array $item Single report item/row.
|
||||
* @return array Key value pair of Column ID => Row Value.
|
||||
*/
|
||||
public function prepare_item_for_export( $item ) {
|
||||
return array(
|
||||
'tax_code' => \WC_Tax::get_rate_code( $item['tax_rate_id'] ),
|
||||
'rate' => $item['tax_rate'],
|
||||
'total_tax' => self::csv_number_format( $item['total_tax'] ),
|
||||
'order_tax' => self::csv_number_format( $item['order_tax'] ),
|
||||
'shipping_tax' => self::csv_number_format( $item['shipping_tax'] ),
|
||||
'orders_count' => $item['orders_count'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,13 +11,20 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Variations;
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
|
||||
|
||||
/**
|
||||
* REST API Reports products controller class.
|
||||
*
|
||||
* @package WooCommerce/API
|
||||
* @extends WC_REST_Reports_Controller
|
||||
*/
|
||||
class Controller extends \WC_REST_Reports_Controller {
|
||||
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
|
||||
/**
|
||||
* Exportable traits.
|
||||
*/
|
||||
use ExportableTraits;
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
|
@ -327,4 +334,61 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock status column export value.
|
||||
*
|
||||
* @param array $status Stock status from report row.
|
||||
* @return string
|
||||
*/
|
||||
protected function _get_stock_status( $status ) {
|
||||
$statuses = wc_get_product_stock_status_options();
|
||||
|
||||
return isset( $statuses[ $status ] ) ? $statuses[ $status ] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column names for export.
|
||||
*
|
||||
* @return array Key value pair of Column ID => Label.
|
||||
*/
|
||||
public function get_export_columns() {
|
||||
$export_columns = array(
|
||||
'product_name' => __( 'Product / Variation Title', 'woocommerce-admin' ),
|
||||
'sku' => __( 'SKU', 'woocommerce-admin' ),
|
||||
'items_sold' => __( 'Items Sold', 'woocommerce-admin' ),
|
||||
'net_revenue' => __( 'N. Revenue', 'woocommerce-admin' ),
|
||||
'orders_count' => __( 'Orders', 'woocommerce-admin' ),
|
||||
);
|
||||
|
||||
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
|
||||
$export_columns['stock_status'] = __( 'Status', 'woocommerce-admin' );
|
||||
$export_columns['stock'] = __( 'Stock', 'woocommerce-admin' );
|
||||
}
|
||||
|
||||
return $export_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column values for export.
|
||||
*
|
||||
* @param array $item Single report item/row.
|
||||
* @return array Key value pair of Column ID => Row Value.
|
||||
*/
|
||||
public function prepare_item_for_export( $item ) {
|
||||
$export_item = array(
|
||||
'product_name' => $item['extended_info']['name'],
|
||||
'sku' => $item['extended_info']['sku'],
|
||||
'items_sold' => $item['items_sold'],
|
||||
'net_revenue' => self::csv_number_format( $item['net_revenue'] ),
|
||||
'orders_count' => $item['orders_count'],
|
||||
);
|
||||
|
||||
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
|
||||
$export_item['stock_status'] = $this->_get_stock_status( $item['extended_info']['stock_status'] );
|
||||
$export_item['stock'] = $item['extended_info']['stock_quantity'];
|
||||
}
|
||||
|
||||
return $export_item;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
exit;
|
||||
}
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
|
||||
|
||||
/**
|
||||
* Include dependencies.
|
||||
*/
|
||||
|
@ -103,6 +105,7 @@ class ReportCSVExporter extends \WC_CSV_Batch_Exporter {
|
|||
* @return bool|WC_REST_Reports_Controller Report controller instance or boolean false on error.
|
||||
*/
|
||||
protected function map_report_controller() {
|
||||
// @todo - Add filter to this list.
|
||||
$controller_map = array(
|
||||
'products' => 'Automattic\WooCommerce\Admin\API\Reports\Products\Controller',
|
||||
'variations' => 'Automattic\WooCommerce\Admin\API\Reports\Variations\Controller',
|
||||
|
@ -113,6 +116,7 @@ class ReportCSVExporter extends \WC_CSV_Batch_Exporter {
|
|||
'stock' => 'Automattic\WooCommerce\Admin\API\Reports\Stock\Controller',
|
||||
'downloads' => 'Automattic\WooCommerce\Admin\API\Reports\Downloads\Controller',
|
||||
'customers' => 'Automattic\WooCommerce\Admin\API\Reports\Customers\Controller',
|
||||
'revenue' => 'Automattic\WooCommerce\Admin\API\Reports\Revenue\Stats\Controller',
|
||||
);
|
||||
|
||||
if ( isset( $controller_map[ $this->report_type ] ) ) {
|
||||
|
@ -129,11 +133,17 @@ class ReportCSVExporter extends \WC_CSV_Batch_Exporter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the report columns from the schema.
|
||||
* Get the report columns from the controller.
|
||||
*
|
||||
* @return array Array of report column names.
|
||||
*/
|
||||
protected function get_report_columns() {
|
||||
// Default to the report's defined export columns.
|
||||
if ( $this->controller instanceof ExportableInterface ) {
|
||||
return $this->controller->get_export_columns();
|
||||
}
|
||||
|
||||
// Fallback to generating columns from the report schema.
|
||||
$report_columns = array();
|
||||
$report_schema = $this->controller->get_item_schema();
|
||||
|
||||
|
@ -190,20 +200,32 @@ class ReportCSVExporter extends \WC_CSV_Batch_Exporter {
|
|||
$request->set_default_params( $defaults );
|
||||
$request->set_query_params( $this->report_args );
|
||||
|
||||
$response = $this->controller->get_items( $request );
|
||||
// Does the controller have an export-specific item retrieval method?
|
||||
// @todo - Potentially revisit. This is only for /revenue/stats/.
|
||||
if ( is_callable( array( $this->controller, 'get_export_items' ) ) ) {
|
||||
$response = $this->controller->get_export_items( $request );
|
||||
} else {
|
||||
$response = $this->controller->get_items( $request );
|
||||
}
|
||||
|
||||
// Use WP_REST_Server::response_to_data() to embed links in data.
|
||||
add_filter( 'woocommerce_rest_check_permissions', '__return_true' );
|
||||
$rest_server = rest_get_server();
|
||||
$report_data = $rest_server->response_to_data( $response, true );
|
||||
remove_filter( 'woocommerce_rest_check_permissions', '__return_true' );
|
||||
|
||||
$report_meta = $response->get_headers();
|
||||
$report_data = $response->get_data();
|
||||
$this->total_rows = $report_meta['X-WP-Total'];
|
||||
$this->row_data = array_map( array( $this, 'generate_row_data' ), $report_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a report item and generate row data from it for export.
|
||||
* Generate row data from a raw report item.
|
||||
*
|
||||
* @param object $item Report item data.
|
||||
* @return array CSV row data.
|
||||
*/
|
||||
protected function generate_row_data( $item ) {
|
||||
protected function get_raw_row_data( $item ) {
|
||||
$columns = $this->get_column_names();
|
||||
$row = array();
|
||||
|
||||
|
@ -236,6 +258,24 @@ class ReportCSVExporter extends \WC_CSV_Batch_Exporter {
|
|||
$row[ $column_id ] = $value;
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the export row for a given report item.
|
||||
*
|
||||
* @param object $item Report item data.
|
||||
* @return array CSV row data.
|
||||
*/
|
||||
protected function generate_row_data( $item ) {
|
||||
// Default to the report's export method.
|
||||
if ( $this->controller instanceof ExportableInterface ) {
|
||||
$row = $this->controller->prepare_item_for_export( $item );
|
||||
} else {
|
||||
// Fallback to raw report data.
|
||||
$row = $this->get_raw_row_data( $item );
|
||||
}
|
||||
|
||||
return apply_filters( "woocommerce_export_{$this->export_type}_row_data", $row, $item );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -274,40 +274,31 @@ class WC_Tests_Reports_Coupons extends WC_Unit_Test_Case {
|
|||
|
||||
// Test the CSV export.
|
||||
$expected_csv_columns = array(
|
||||
'coupon_id',
|
||||
'amount',
|
||||
'orders_count',
|
||||
'code',
|
||||
'date_created',
|
||||
'date_created_gmt',
|
||||
'date_expires',
|
||||
'date_expires_gmt',
|
||||
'discount_type',
|
||||
'"Coupon Code"',
|
||||
'Orders',
|
||||
'"Amount Discounted"',
|
||||
'Created',
|
||||
'Expires',
|
||||
'Type',
|
||||
);
|
||||
|
||||
// Expected CSV for Coupon 2.
|
||||
$coupon_2_csv = array(
|
||||
$coupon_2_response['coupon_id'],
|
||||
$coupon_2_response['amount'],
|
||||
$coupon_2_response['orders_count'],
|
||||
$coupon_2_response['extended_info']['code'],
|
||||
$coupon_2_response['orders_count'],
|
||||
$coupon_2_response['amount'],
|
||||
$coupon_2_response['extended_info']['date_created'],
|
||||
$coupon_2_response['extended_info']['date_created_gmt'],
|
||||
$coupon_2_response['extended_info']['date_expires'],
|
||||
$coupon_2_response['extended_info']['date_expires_gmt'],
|
||||
$coupon_2_response['extended_info']['date_expires'] ? : 'N/A',
|
||||
$coupon_2_response['extended_info']['discount_type'],
|
||||
);
|
||||
|
||||
// Expected CSV for Coupon 1.
|
||||
$coupon_1_csv = array(
|
||||
$coupon_1_response['coupon_id'],
|
||||
$coupon_1_response['amount'],
|
||||
$coupon_1_response['orders_count'],
|
||||
$coupon_1_response['extended_info']['code'],
|
||||
$coupon_1_response['orders_count'],
|
||||
$coupon_1_response['amount'],
|
||||
$coupon_1_response['extended_info']['date_created'],
|
||||
$coupon_1_response['extended_info']['date_created_gmt'],
|
||||
$coupon_1_response['extended_info']['date_expires'],
|
||||
$coupon_1_response['extended_info']['date_expires_gmt'],
|
||||
$coupon_1_response['extended_info']['date_expires'] ? : 'N/A',
|
||||
$coupon_1_response['extended_info']['discount_type'],
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in New Issue