Add Variations Report (https://github.com/woocommerce/woocommerce-admin/pull/5167)
* Add initial Variations Report to analytics feature. * Restrict query to variations when not specifying any product IDs. * Add route to get variations without specifying a parent. * Move variations table component to variations report directory. * Add missing LIMIT clause to variations report query. * Remove broken features from Variations table. * Add Variations report controller to CSV emailer. * Add initial Variation Stats endpoint, based on Product Stats. * Hook Variations Report components up to new stats endpoint. * Hook attribute filter up to variations report queries. * Remove variation title filter usage. See: https://github.com/woocommerce/woocommerce-admin/pull/5100 * Use filtered separator in variation name formatting. * Add "single variation" to variations report, fix autocompleter API request. * Fix segmentation by variation. * Add comparison to variations report. * Always include manually specified variations in report results. * Fix variations report table comparison mode. The ReportTable component expects the `filter` query param. * Fixing styling of compare button without table search component. * Add variation filter to Orders report. * Link orders count to orders report filtered by variation. * Orders report: include variation attributes in product names. * Further style tweaks for variations report download button. * Add variations filter to order stats query. * Clean up "category includes" login in REST controllers. Prep for "category excludes" in the Variations report. * Support category exclusion in report filters. * Fix filter param used by the variation report table component. * Add category filter to variations report. * Fix initial selected ReportTable rows when using non-default compareParam. * Add a new autocompleter for variable products. * Add products filter to variations report. * Fix tests. * Handle variation IDs that are no longer found. * Add documentation. * Use getSetting() instead of directly accessing window properties in client code. * Fix ordering Variations by SKU.
|
@ -59,6 +59,7 @@ const ReportTable = ( props ) => {
|
|||
// eslint-disable-next-line no-unused-vars
|
||||
tableQuery,
|
||||
compareBy,
|
||||
compareParam,
|
||||
searchBy,
|
||||
labels = {},
|
||||
...tableProps
|
||||
|
@ -69,7 +70,7 @@ const ReportTable = ( props ) => {
|
|||
|
||||
const { items, query: reportQuery } = tableData;
|
||||
|
||||
const initialSelectedRows = query.filter
|
||||
const initialSelectedRows = query[ compareParam ]
|
||||
? getIdsFromQuery( query[ compareBy ] )
|
||||
: [];
|
||||
const [ selectedRows, setSelectedRows ] = useState( initialSelectedRows );
|
||||
|
@ -211,7 +212,6 @@ const ReportTable = ( props ) => {
|
|||
};
|
||||
|
||||
const onCompare = () => {
|
||||
const { compareParam } = props;
|
||||
if ( compareBy ) {
|
||||
onQueryChange( 'compare' )(
|
||||
compareBy,
|
||||
|
@ -222,7 +222,7 @@ const ReportTable = ( props ) => {
|
|||
};
|
||||
|
||||
const onSearchChange = ( values ) => {
|
||||
const { baseSearchQuery, compareParam } = props;
|
||||
const { baseSearchQuery } = props;
|
||||
// A comma is used as a separator between search terms, so we want to escape
|
||||
// any comma they contain.
|
||||
const searchTerms = values.map( ( v ) =>
|
||||
|
@ -583,12 +583,8 @@ export default compose(
|
|||
SETTINGS_STORE_NAME
|
||||
).getSetting( 'wc_admin', 'wcAdminSettings' );
|
||||
|
||||
// Variations and Category charts are powered by the /reports/products/stats endpoint.
|
||||
const chartEndpoint = [ 'variations', 'categories' ].includes(
|
||||
endpoint
|
||||
)
|
||||
? 'products'
|
||||
: endpoint;
|
||||
// Category charts are powered by the /reports/products/stats endpoint.
|
||||
const chartEndpoint = endpoint === 'categories' ? 'products' : endpoint;
|
||||
const primaryData = getSummary
|
||||
? getReportChartData( {
|
||||
endpoint: chartEndpoint,
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
|
||||
.woocommerce-report-table__scroll-point {
|
||||
position: relative;
|
||||
top: -#{$adminbar-height + $gap};
|
||||
|
@ -68,6 +66,28 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.has-compare:not(.has-search) {
|
||||
.woocommerce-table__download-button {
|
||||
align-self: center;
|
||||
grid-column-start: 3;
|
||||
}
|
||||
|
||||
@include breakpoint( '<960px' ) {
|
||||
.woocommerce-table__download-button {
|
||||
grid-area: 1 / 2 / 2 / 3;
|
||||
}
|
||||
.woocommerce-card__action {
|
||||
grid-template-columns: auto;
|
||||
|
||||
.woocommerce-table__compare {
|
||||
grid-area: 1 / 2 / 1 / 2;
|
||||
justify-self: left;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-search:not(.has-compare) {
|
||||
.woocommerce-card__action {
|
||||
grid-template-columns: 1fr auto;
|
||||
|
|
|
@ -22,6 +22,11 @@ const RevenueReport = lazy( () =>
|
|||
const ProductsReport = lazy( () =>
|
||||
import( /* webpackChunkName: "analytics-report-products" */ './products' )
|
||||
);
|
||||
const VariationsReport = lazy( () =>
|
||||
import(
|
||||
/* webpackChunkName: "analytics-report-variations" */ './variations'
|
||||
)
|
||||
);
|
||||
const OrdersReport = lazy( () =>
|
||||
import( /* webpackChunkName: "analytics-report-orders" */ './orders' )
|
||||
);
|
||||
|
@ -58,6 +63,11 @@ export default () => {
|
|||
title: __( 'Products', 'woocommerce-admin' ),
|
||||
component: ProductsReport,
|
||||
},
|
||||
{
|
||||
report: 'variations',
|
||||
title: __( 'Variations', 'woocommerce-admin' ),
|
||||
component: VariationsReport,
|
||||
},
|
||||
{
|
||||
report: 'orders',
|
||||
title: __( 'Orders', 'woocommerce-admin' ),
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
getCouponLabels,
|
||||
getProductLabels,
|
||||
getTaxRateLabels,
|
||||
getVariationLabels,
|
||||
} from '../../../lib/async-requests';
|
||||
|
||||
const ORDERS_REPORT_CHARTS_FILTER = 'woocommerce_admin_orders_report_charts';
|
||||
|
@ -156,6 +157,51 @@ export const advancedFilters = applyFilters(
|
|||
getLabels: getProductLabels,
|
||||
},
|
||||
},
|
||||
variation: {
|
||||
labels: {
|
||||
add: __( 'Variations', 'woocommerce-admin' ),
|
||||
placeholder: __( 'Search variations', 'woocommerce-admin' ),
|
||||
remove: __(
|
||||
'Remove variations filter',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
rule: __(
|
||||
'Select a variation filter match',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
/* translators: A sentence describing a Variation filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||
title: __(
|
||||
'{{title}}Variation{{/title}} {{rule /}} {{filter /}}',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
filter: __( 'Select variation', 'woocommerce-admin' ),
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
value: 'includes',
|
||||
/* translators: Sentence fragment, logical, "Includes" refers to orders including a given variation(s). Screenshot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||
label: _x(
|
||||
'Includes',
|
||||
'variations',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'excludes',
|
||||
/* translators: Sentence fragment, logical, "Excludes" refers to orders excluding a given variation(s). Screenshot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||
label: _x(
|
||||
'Excludes',
|
||||
'variations',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
},
|
||||
],
|
||||
input: {
|
||||
component: 'Search',
|
||||
type: 'variations',
|
||||
getLabels: getVariationLabels,
|
||||
},
|
||||
},
|
||||
coupon: {
|
||||
labels: {
|
||||
add: __( 'Coupon Codes', 'woocommerce-admin' ),
|
||||
|
|
|
@ -17,7 +17,7 @@ import ProductsReportTable from './table';
|
|||
import ReportChart from '../../components/report-chart';
|
||||
import ReportError from '../../components/report-error';
|
||||
import ReportSummary from '../../components/report-summary';
|
||||
import VariationsReportTable from './table-variations';
|
||||
import VariationsReportTable from '../variations/table';
|
||||
import ReportFilters from '../../components/report-filters';
|
||||
|
||||
class ProductsReport extends Component {
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _x } from '@wordpress/i18n';
|
||||
import { applyFilters } from '@wordpress/hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
getCategoryLabels,
|
||||
getProductLabels,
|
||||
getVariationLabels,
|
||||
} from '../../../lib/async-requests';
|
||||
|
||||
const VARIATIONS_REPORT_CHARTS_FILTER =
|
||||
'woocommerce_admin_variations_report_charts';
|
||||
const VARIATIONS_REPORT_FILTERS_FILTER =
|
||||
'woocommerce_admin_variations_report_filters';
|
||||
const VARIATIONS_REPORT_ADVANCED_FILTERS_FILTER =
|
||||
'woocommerce_admin_variations_report_advanced_filters';
|
||||
|
||||
export const charts = applyFilters( VARIATIONS_REPORT_CHARTS_FILTER, [
|
||||
{
|
||||
key: 'items_sold',
|
||||
label: __( 'Items Sold', 'woocommerce-admin' ),
|
||||
order: 'desc',
|
||||
orderby: 'items_sold',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
key: 'net_revenue',
|
||||
label: __( 'Net Sales', 'woocommerce-admin' ),
|
||||
order: 'desc',
|
||||
orderby: 'net_revenue',
|
||||
type: 'currency',
|
||||
},
|
||||
{
|
||||
key: 'orders_count',
|
||||
label: __( 'Orders', 'woocommerce-admin' ),
|
||||
order: 'desc',
|
||||
orderby: 'orders_count',
|
||||
type: 'number',
|
||||
},
|
||||
] );
|
||||
|
||||
export const filters = applyFilters( VARIATIONS_REPORT_FILTERS_FILTER, [
|
||||
{
|
||||
label: __( 'Show', 'woocommerce-admin' ),
|
||||
staticParams: [ 'chartType', 'paged', 'per_page' ],
|
||||
param: 'filter-variations',
|
||||
showFilters: () => true,
|
||||
filters: [
|
||||
{
|
||||
label: __( 'All Variations', 'woocommerce-admin' ),
|
||||
chartMode: 'item-comparison',
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
label: __( 'Single Variation', 'woocommerce-admin' ),
|
||||
value: 'select_variation',
|
||||
subFilters: [
|
||||
{
|
||||
component: 'Search',
|
||||
value: 'single_variation',
|
||||
path: [ 'select_variation' ],
|
||||
settings: {
|
||||
type: 'variations',
|
||||
param: 'variations',
|
||||
getLabels: getVariationLabels,
|
||||
labels: {
|
||||
placeholder: __(
|
||||
'Type to search for a variation',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
button: __(
|
||||
'Single Variation',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: __( 'Comparison', 'woocommerce-admin' ),
|
||||
chartMode: 'item-comparison',
|
||||
value: 'compare-variations',
|
||||
settings: {
|
||||
type: 'variations',
|
||||
param: 'variations',
|
||||
getLabels: getVariationLabels,
|
||||
labels: {
|
||||
helpText: __(
|
||||
'Check at least two variations below to compare',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
placeholder: __(
|
||||
'Search for variations to compare',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
title: __( 'Compare Variations', 'woocommerce-admin' ),
|
||||
update: __( 'Compare', 'woocommerce-admin' ),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __( 'Advanced Filters', 'woocommerce-admin' ),
|
||||
value: 'advanced',
|
||||
},
|
||||
],
|
||||
},
|
||||
] );
|
||||
|
||||
export const advancedFilters = applyFilters(
|
||||
VARIATIONS_REPORT_ADVANCED_FILTERS_FILTER,
|
||||
{
|
||||
title: _x(
|
||||
'Variations Match {{select /}} Filters',
|
||||
'A sentence describing filters for Variations. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
filters: {
|
||||
attribute: {
|
||||
allowMultiple: true,
|
||||
labels: {
|
||||
add: __( 'Attribute', 'woocommerce-admin' ),
|
||||
placeholder: __( 'Search attributes', 'woocommerce-admin' ),
|
||||
remove: __(
|
||||
'Remove attribute filter',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
rule: __(
|
||||
'Select a product attribute filter match',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||
title: __(
|
||||
'{{title}}Attribute{{/title}} {{rule /}} {{filter /}}',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
filter: __( 'Select attributes', 'woocommerce-admin' ),
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
value: 'is',
|
||||
/* translators: Sentence fragment, logical, "Is" refers to searching for product variations matching a chosen attribute. Screenshot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||
label: _x(
|
||||
'Is',
|
||||
'product attribute',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'is_not',
|
||||
/* translators: Sentence fragment, logical, "Is Not" refers to searching for product variations that don\'t match a chosen attribute. Screenshot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||
label: _x(
|
||||
'Is Not',
|
||||
'product attribute',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
},
|
||||
],
|
||||
input: {
|
||||
component: 'ProductAttribute',
|
||||
},
|
||||
},
|
||||
category: {
|
||||
labels: {
|
||||
add: __( 'Categories', 'woocommerce-admin' ),
|
||||
placeholder: __( 'Search categories', 'woocommerce-admin' ),
|
||||
remove: __(
|
||||
'Remove categories filter',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
rule: __(
|
||||
'Select a category filter match',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
/* translators: A sentence describing a Category filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||
title: __(
|
||||
'{{title}}Category{{/title}} {{rule /}} {{filter /}}',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
filter: __( 'Select categories', 'woocommerce-admin' ),
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
value: 'includes',
|
||||
/* translators: Sentence fragment, logical, "Includes" refers to variations including a given category. Screenshot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||
label: _x(
|
||||
'Includes',
|
||||
'categories',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'excludes',
|
||||
/* translators: Sentence fragment, logical, "Excludes" refers to variations excluding a given category. Screenshot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||
label: _x(
|
||||
'Excludes',
|
||||
'categories',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
},
|
||||
],
|
||||
input: {
|
||||
component: 'Search',
|
||||
type: 'categories',
|
||||
getLabels: getCategoryLabels,
|
||||
},
|
||||
},
|
||||
product: {
|
||||
labels: {
|
||||
add: __( 'Products', 'woocommerce-admin' ),
|
||||
placeholder: __( 'Search products', 'woocommerce-admin' ),
|
||||
remove: __( 'Remove products filter', 'woocommerce-admin' ),
|
||||
rule: __(
|
||||
'Select a product filter match',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||
title: __(
|
||||
'{{title}}Product{{/title}} {{rule /}} {{filter /}}',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
filter: __( 'Select products', 'woocommerce-admin' ),
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
value: 'includes',
|
||||
/* translators: Sentence fragment, logical, "Includes" refers to orders including a given product(s). Screenshot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||
label: _x(
|
||||
'Includes',
|
||||
'products',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'excludes',
|
||||
/* translators: Sentence fragment, logical, "Excludes" refers to orders excluding a given product(s). Screenshot for context: https://cloudup.com/cSsUY9VeCVJ */
|
||||
label: _x(
|
||||
'Excludes',
|
||||
'products',
|
||||
'woocommerce-admin'
|
||||
),
|
||||
},
|
||||
],
|
||||
input: {
|
||||
component: 'Search',
|
||||
type: 'variableProducts',
|
||||
getLabels: getProductLabels,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { advancedFilters, charts, filters } from './config';
|
||||
import getSelectedChart from '../../../lib/get-selected-chart';
|
||||
import ReportChart from '../../components/report-chart';
|
||||
import ReportError from '../../components/report-error';
|
||||
import ReportSummary from '../../components/report-summary';
|
||||
import VariationsReportTable from './table';
|
||||
import ReportFilters from '../../components/report-filters';
|
||||
|
||||
const getChartMeta = ( { query } ) => {
|
||||
const isCompareView =
|
||||
query[ 'filter-variations' ] === 'compare-variations' &&
|
||||
query.variations &&
|
||||
query.variations.split( ',' ).length > 1;
|
||||
|
||||
return {
|
||||
compareObject: 'variations',
|
||||
itemsLabel: __( '%d variations', 'woocommerce-admin' ),
|
||||
mode: isCompareView ? 'item-comparison' : 'time-comparison',
|
||||
};
|
||||
};
|
||||
|
||||
const VariationsReport = ( props ) => {
|
||||
const { itemsLabel, mode } = getChartMeta( props );
|
||||
const { path, query, isError, isRequesting } = props;
|
||||
|
||||
if ( isError ) {
|
||||
return <ReportError isError />;
|
||||
}
|
||||
|
||||
const chartQuery = {
|
||||
...query,
|
||||
};
|
||||
|
||||
if ( mode === 'item-comparison' ) {
|
||||
chartQuery.segmentby = 'variation';
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ReportFilters
|
||||
query={ query }
|
||||
path={ path }
|
||||
filters={ filters }
|
||||
advancedFilters={ advancedFilters }
|
||||
report="variations"
|
||||
/>
|
||||
<ReportSummary
|
||||
mode={ mode }
|
||||
charts={ charts }
|
||||
endpoint="variations"
|
||||
isRequesting={ isRequesting }
|
||||
query={ chartQuery }
|
||||
selectedChart={ getSelectedChart( query.chart, charts ) }
|
||||
filters={ filters }
|
||||
advancedFilters={ advancedFilters }
|
||||
/>
|
||||
<ReportChart
|
||||
charts={ charts }
|
||||
mode={ mode }
|
||||
filters={ filters }
|
||||
advancedFilters={ advancedFilters }
|
||||
endpoint="variations"
|
||||
isRequesting={ isRequesting }
|
||||
itemsLabel={ itemsLabel }
|
||||
path={ path }
|
||||
query={ chartQuery }
|
||||
selectedChart={ getSelectedChart( chartQuery.chart, charts ) }
|
||||
/>
|
||||
<VariationsReportTable
|
||||
isRequesting={ isRequesting }
|
||||
query={ query }
|
||||
filters={ filters }
|
||||
advancedFilters={ advancedFilters }
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
VariationsReport.propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
query: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default VariationsReport;
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
import { __, _n, _x } from '@wordpress/i18n';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { map, get } from 'lodash';
|
||||
import { map } from 'lodash';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
|
||||
import { formatValue } from '@woocommerce/number';
|
||||
|
@ -13,18 +13,15 @@ import { getAdminLink, getSetting } from '@woocommerce/wc-admin-settings';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import ReportTable from '../../components/report-table';
|
||||
import { isLowStock } from './utils';
|
||||
import { isLowStock } from '../products/utils';
|
||||
import { CurrencyContext } from '../../../lib/currency-context';
|
||||
import { getVariationName } from '../../../lib/async-requests';
|
||||
|
||||
const manageStock = getSetting( 'manageStock', 'no' );
|
||||
const stockStatuses = getSetting( 'stockStatuses', {} );
|
||||
|
||||
const getFullVariationName = ( rowData ) =>
|
||||
get( rowData, [ 'extended_info', 'name' ], '' ) +
|
||||
' - ' +
|
||||
get( rowData, [ 'extended_info', 'attributes' ], [] )
|
||||
.map( ( { option } ) => option )
|
||||
.join( ', ' );
|
||||
getVariationName( rowData.extended_info || {} );
|
||||
|
||||
class VariationsReportTable extends Component {
|
||||
constructor() {
|
||||
|
@ -102,6 +99,7 @@ class VariationsReportTable extends Component {
|
|||
net_revenue: netRevenue,
|
||||
orders_count: ordersCount,
|
||||
product_id: productId,
|
||||
variation_id: variationId,
|
||||
} = row;
|
||||
const extendedInfo = row.extended_info || {};
|
||||
const {
|
||||
|
@ -116,7 +114,7 @@ class VariationsReportTable extends Component {
|
|||
'/analytics/orders',
|
||||
{
|
||||
filter: 'advanced',
|
||||
product_includes: query.products,
|
||||
variation_includes: variationId,
|
||||
}
|
||||
);
|
||||
const editPostLink = getAdminLink(
|
||||
|
@ -253,8 +251,8 @@ class VariationsReportTable extends Component {
|
|||
return (
|
||||
<ReportTable
|
||||
baseSearchQuery={ baseSearchQuery }
|
||||
compareBy={ 'variations' }
|
||||
compareParam={ 'filter-variations' }
|
||||
compareBy="variations"
|
||||
compareParam="filter-variations"
|
||||
endpoint="variations"
|
||||
getHeadersContent={ this.getHeadersContent }
|
||||
getRowsContent={ this.getRowsContent }
|
||||
|
@ -269,7 +267,6 @@ class VariationsReportTable extends Component {
|
|||
'net_revenue',
|
||||
'orders_count',
|
||||
] }
|
||||
searchBy="variations"
|
||||
tableQuery={ {
|
||||
orderby: query.orderby || 'items_sold',
|
||||
order: query.order || 'desc',
|
|
@ -6,6 +6,7 @@ import apiFetch from '@wordpress/api-fetch';
|
|||
import { identity } from 'lodash';
|
||||
import { getIdsFromQuery } from '@woocommerce/navigation';
|
||||
import { NAMESPACE } from '@woocommerce/data';
|
||||
import { getSetting } from '@woocommerce/wc-admin-settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -77,14 +78,42 @@ export const getTaxRateLabels = getRequestByIdString(
|
|||
} )
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a variation name by concatenating each of the variation's
|
||||
* attribute option strings.
|
||||
*
|
||||
* @param {Object} variation - variation returned by the api
|
||||
* @param {Array} variation.attributes - attribute objects, with option property.
|
||||
* @param {string} variation.name - name of variation.
|
||||
* @return {string} - formatted variation name
|
||||
*/
|
||||
export function getVariationName( { attributes, name } ) {
|
||||
const separator = getSetting( 'variationTitleAttributesSeparator', ' - ' );
|
||||
|
||||
if ( name.indexOf( separator ) > -1 ) {
|
||||
return name;
|
||||
}
|
||||
|
||||
const attributeList = attributes
|
||||
.map( ( { option } ) => option )
|
||||
.join( ', ' );
|
||||
|
||||
return attributeList ? name + separator + attributeList : name;
|
||||
}
|
||||
|
||||
export const getVariationLabels = getRequestByIdString(
|
||||
( query ) => NAMESPACE + `/products/${ query.products }/variations`,
|
||||
( { products } ) => {
|
||||
// If a product was specified, get just its variations.
|
||||
if ( products ) {
|
||||
return NAMESPACE + `/products/${ products }/variations`;
|
||||
}
|
||||
|
||||
return NAMESPACE + '/variations';
|
||||
},
|
||||
( variation ) => {
|
||||
return {
|
||||
key: variation.id,
|
||||
label: variation.attributes
|
||||
.map( ( { option } ) => option )
|
||||
.join( ', ' ),
|
||||
label: getVariationName( variation ),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# Variations Report
|
||||
|
||||
The Variations Report provides insight into the sales performance of each Product Variation on your store.
|
||||
|
||||
### All Variations View
|
||||
|
||||
By default, the Variations Report displays the All Variations view. All Variations that have had sales in the specified date range will be shown.
|
||||
|
||||
![Variations Report All Variations View](images/analytics-variations-report.png)
|
||||
|
||||
### Single Variation View
|
||||
|
||||
![Variations Report Single Variation Search](images/analytics-variations-report-single-variation-search.png)
|
||||
|
||||
By selecting "Single Variation", you can search for a single variation to display report data for.
|
||||
|
||||
![Variations Report Single Variation View](images/analytics-variations-report-single-variation.png)
|
||||
|
||||
### Comparison Mode
|
||||
|
||||
![Variations Report Comparison Mode Search](images/analytics-variations-report-comparison-search.png)
|
||||
|
||||
By selecting "Comparison", you can search for multiple variations to display report data for.
|
||||
|
||||
![Variations Report Comparison Mode](images/analytics-variations-report-comparison.png)
|
||||
|
||||
You can also use the checkboxes in the report table to select variations for comparison. Click "Compare" in the table header to compare the selected variations.
|
||||
|
||||
### Advanced Filters
|
||||
|
||||
The advanced filters allow adding multiple filters to the report. These filters can be applied in two ways:
|
||||
|
||||
- All - Variations must match all filters to be included in the report
|
||||
- Any - Variations must match one or more filters to be included in the report
|
||||
|
||||
![Orders Report Filter Matching](images/analytics-variations-filter-match.png)
|
||||
|
||||
The following fields can be used for filtering:
|
||||
|
||||
- Attribute (can be used multiple times)
|
||||
- Product (parent product)
|
||||
- Category
|
||||
|
||||
![Variations Report Advanced Filters](images/analytics-variations-report-advanced-filters.png)
|
||||
|
||||
### Report Columns
|
||||
|
||||
The report table contains the following columns:
|
||||
|
||||
- Variation Title - links to Edit Product screen
|
||||
- SKU
|
||||
- Items Sold (count)
|
||||
- Net Sales
|
||||
- Orders (count) - links to Orders Report filtered by Variation
|
||||
- Status (in/out stock)
|
||||
- Stock (inventory quantity)
|
||||
|
||||
### Report Sorting
|
||||
|
||||
The report table allows sorting by the following columns:
|
||||
|
||||
- SKU
|
||||
- Items Sold (count)
|
||||
- Net Sales
|
||||
- Orders (count)
|
||||
|
||||
By default, the report sorts Variations by most items sold.
|
||||
|
||||
#### Clarifying Terms
|
||||
|
||||
"Net Sales" is calculated by subtracting refunds and coupons from the sale price of the variation(s).
|
||||
|
||||
As an equation, it might look like: `(variation price * quantity) - refunds - coupons`.
|
||||
|
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 138 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 103 KiB |
After Width: | Height: | Size: 147 KiB |
|
@ -11,4 +11,5 @@ export { default as orders } from './orders';
|
|||
export { default as product } from './product';
|
||||
export { default as taxes } from './taxes';
|
||||
export { default as usernames } from './usernames';
|
||||
export { default as variableProduct } from './variable-product';
|
||||
export { default as variations } from './variations';
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { addQueryArgs } from '@wordpress/url';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import productsAutocompleter from './product';
|
||||
|
||||
/**
|
||||
* A raw completer option.
|
||||
*
|
||||
* @typedef {*} CompleterOption
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback FnGetOptions
|
||||
*
|
||||
* @return {(CompleterOption[]|Promise.<CompleterOption[]>)} The completer options or a promise for them.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback FnGetOptionKeywords
|
||||
* @param {CompleterOption} option a completer option.
|
||||
*
|
||||
* @return {string[]} list of key words to search.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback FnIsOptionDisabled
|
||||
* @param {CompleterOption} option a completer option.
|
||||
*
|
||||
* @return {string[]} whether or not the given option is disabled.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback FnGetOptionLabel
|
||||
* @param {CompleterOption} option a completer option.
|
||||
*
|
||||
* @return {(string|Array.<(string|Node)>)} list of react components to render.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback FnAllowContext
|
||||
* @param {string} before the string before the auto complete trigger and query.
|
||||
* @param {string} after the string after the autocomplete trigger and query.
|
||||
*
|
||||
* @return {boolean} true if the completer can handle.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} OptionCompletion
|
||||
* @property {'insert-at-caret'|'replace'} action the intended placement of the completion.
|
||||
* @property {OptionCompletionValue} value the completion value.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A completion value.
|
||||
*
|
||||
* @typedef {(string|WPElement|Object)} OptionCompletionValue
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback FnGetOptionCompletion
|
||||
* @param {CompleterOption} value the value of the completer option.
|
||||
* @param {string} query the text value of the autocomplete query.
|
||||
*
|
||||
* @return {(OptionCompletion|OptionCompletionValue)} the completion for the given option. If an
|
||||
* OptionCompletionValue is returned, the
|
||||
* completion action defaults to `insert-at-caret`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} WPCompleter
|
||||
* @property {string} name a way to identify a completer, useful for selective overriding.
|
||||
* @property {?string} className A class to apply to the popup menu.
|
||||
* @property {string} triggerPrefix the prefix that will display the menu.
|
||||
* @property {(CompleterOption[]|FnGetOptions)} options the completer options or a function to get them.
|
||||
* @property {?FnGetOptionKeywords} getOptionKeywords get the keywords for a given option.
|
||||
* @property {?FnIsOptionDisabled} isOptionDisabled get whether or not the given option is disabled.
|
||||
* @property {FnGetOptionLabel} getOptionLabel get the label for a given option.
|
||||
* @property {?FnAllowContext} allowContext filter the context under which the autocomplete activates.
|
||||
* @property {FnGetOptionCompletion} getOptionCompletion get the completion associated with a given option.
|
||||
*/
|
||||
/**
|
||||
* A variable products completer.
|
||||
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
|
||||
*
|
||||
* @type {WPCompleter}
|
||||
*/
|
||||
export default {
|
||||
...productsAutocompleter,
|
||||
name: 'products',
|
||||
options( search ) {
|
||||
const query = search
|
||||
? {
|
||||
search,
|
||||
per_page: 10,
|
||||
orderby: 'popularity',
|
||||
type: 'variable',
|
||||
}
|
||||
: {};
|
||||
return apiFetch( {
|
||||
path: addQueryArgs( '/wc-analytics/products', query ),
|
||||
} );
|
||||
},
|
||||
};
|
|
@ -93,41 +93,69 @@ import ProductImage from '../../product-image';
|
|||
* attribute option strings.
|
||||
*
|
||||
* @param {Object} variation - variation returned by the api
|
||||
* @return {string} - variation name
|
||||
* @param {Array} variation.attributes - attribute objects, with option property.
|
||||
* @param {string} variation.name - name of variation.
|
||||
* @return {string} - formatted variation name
|
||||
*/
|
||||
function getVariationName( variation ) {
|
||||
return variation.attributes.map( ( { option } ) => option ).join( ', ' );
|
||||
function getVariationName( { attributes, name } ) {
|
||||
const separator =
|
||||
window.wcSettings.variationTitleAttributesSeparator || ' - ';
|
||||
|
||||
if ( name.indexOf( separator ) > -1 ) {
|
||||
return name;
|
||||
}
|
||||
|
||||
const attributeList = attributes
|
||||
.map( ( { option } ) => option )
|
||||
.join( ', ' );
|
||||
|
||||
return attributeList ? name + separator + attributeList : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* A products completer.
|
||||
* A variations completer.
|
||||
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
|
||||
*
|
||||
* @type {WPCompleter}
|
||||
*/
|
||||
export default {
|
||||
name: 'products',
|
||||
name: 'variations',
|
||||
className: 'woocommerce-search__product-result',
|
||||
options( search ) {
|
||||
const query = search
|
||||
? {
|
||||
search,
|
||||
per_page: 30,
|
||||
_fields: [ 'id', 'sku', 'description', 'attributes' ],
|
||||
_fields: [
|
||||
'attributes',
|
||||
'description',
|
||||
'id',
|
||||
'name',
|
||||
'sku',
|
||||
],
|
||||
}
|
||||
: {};
|
||||
const product = getQuery().products;
|
||||
if ( ! product || product.includes( ',' ) ) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Invalid product id supplied to Variations autocompleter'
|
||||
);
|
||||
|
||||
// Product was specified, search only its variations.
|
||||
if ( product ) {
|
||||
if ( product.includes( ',' ) ) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Invalid product id supplied to Variations autocompleter'
|
||||
);
|
||||
}
|
||||
return apiFetch( {
|
||||
path: addQueryArgs(
|
||||
`/wc-analytics/products/${ product }/variations`,
|
||||
query
|
||||
),
|
||||
} );
|
||||
}
|
||||
|
||||
// Product was not specified, search all variations.
|
||||
return apiFetch( {
|
||||
path: addQueryArgs(
|
||||
`/wc-analytics/products/${ product }/variations`,
|
||||
query
|
||||
),
|
||||
path: addQueryArgs( '/wc-analytics/variations', query ),
|
||||
} );
|
||||
},
|
||||
isDebounced: true,
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
productCategory,
|
||||
taxes,
|
||||
usernames,
|
||||
variableProduct,
|
||||
variations,
|
||||
} from './autocompleters';
|
||||
|
||||
|
@ -61,6 +62,8 @@ export class Search extends Component {
|
|||
return taxes;
|
||||
case 'usernames':
|
||||
return usernames;
|
||||
case 'variableProducts':
|
||||
return variableProduct;
|
||||
case 'variations':
|
||||
return variations;
|
||||
case 'custom':
|
||||
|
@ -209,6 +212,7 @@ Search.propTypes = {
|
|||
'products',
|
||||
'taxes',
|
||||
'usernames',
|
||||
'variableProducts',
|
||||
'variations',
|
||||
'custom',
|
||||
] ).isRequired,
|
||||
|
|
|
@ -74,6 +74,7 @@ class Init {
|
|||
'Automattic\WooCommerce\Admin\API\Reports\Products\Controller',
|
||||
'Automattic\WooCommerce\Admin\API\Reports\Variations\Controller',
|
||||
'Automattic\WooCommerce\Admin\API\Reports\Products\Stats\Controller',
|
||||
'Automattic\WooCommerce\Admin\API\Reports\Variations\Stats\Controller',
|
||||
'Automattic\WooCommerce\Admin\API\Reports\Revenue\Stats\Controller',
|
||||
'Automattic\WooCommerce\Admin\API\Reports\Orders\Controller',
|
||||
'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Controller',
|
||||
|
@ -117,23 +118,24 @@ class Init {
|
|||
return array_merge(
|
||||
$data_stores,
|
||||
array(
|
||||
'report-revenue-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore',
|
||||
'report-orders' => 'Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore',
|
||||
'report-orders-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore',
|
||||
'report-products' => 'Automattic\WooCommerce\Admin\API\Reports\Products\DataStore',
|
||||
'report-variations' => 'Automattic\WooCommerce\Admin\API\Reports\Variations\DataStore',
|
||||
'report-products-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Products\Stats\DataStore',
|
||||
'report-categories' => 'Automattic\WooCommerce\Admin\API\Reports\Categories\DataStore',
|
||||
'report-taxes' => 'Automattic\WooCommerce\Admin\API\Reports\Taxes\DataStore',
|
||||
'report-taxes-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\DataStore',
|
||||
'report-coupons' => 'Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore',
|
||||
'report-coupons-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\DataStore',
|
||||
'report-downloads' => 'Automattic\WooCommerce\Admin\API\Reports\Downloads\DataStore',
|
||||
'report-downloads-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats\DataStore',
|
||||
'admin-note' => 'Automattic\WooCommerce\Admin\Notes\DataStore',
|
||||
'report-customers' => 'Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore',
|
||||
'report-customers-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\DataStore',
|
||||
'report-stock-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\DataStore',
|
||||
'report-revenue-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore',
|
||||
'report-orders' => 'Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore',
|
||||
'report-orders-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore',
|
||||
'report-products' => 'Automattic\WooCommerce\Admin\API\Reports\Products\DataStore',
|
||||
'report-variations' => 'Automattic\WooCommerce\Admin\API\Reports\Variations\DataStore',
|
||||
'report-products-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Products\Stats\DataStore',
|
||||
'report-variations-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Variations\Stats\DataStore',
|
||||
'report-categories' => 'Automattic\WooCommerce\Admin\API\Reports\Categories\DataStore',
|
||||
'report-taxes' => 'Automattic\WooCommerce\Admin\API\Reports\Taxes\DataStore',
|
||||
'report-taxes-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\DataStore',
|
||||
'report-coupons' => 'Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore',
|
||||
'report-coupons-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\DataStore',
|
||||
'report-downloads' => 'Automattic\WooCommerce\Admin\API\Reports\Downloads\DataStore',
|
||||
'report-downloads-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats\DataStore',
|
||||
'admin-note' => 'Automattic\WooCommerce\Admin\Notes\DataStore',
|
||||
'report-customers' => 'Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore',
|
||||
'report-customers-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\DataStore',
|
||||
'report-stock-stats' => 'Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\DataStore',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,28 @@ class ProductVariations extends \WC_REST_Product_Variations_Controller {
|
|||
*/
|
||||
protected $namespace = 'wc-analytics';
|
||||
|
||||
/**
|
||||
* Register the routes for products.
|
||||
*/
|
||||
public function register_routes() {
|
||||
parent::register_routes();
|
||||
|
||||
// Add a route for listing variations without specifying the parent product ID.
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/variations',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_items' ),
|
||||
'permission_callback' => array( $this, 'get_items_permissions_check' ),
|
||||
'args' => $this->get_collection_params(),
|
||||
),
|
||||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query params for collections.
|
||||
*
|
||||
|
@ -104,6 +126,11 @@ class ProductVariations extends \WC_REST_Product_Variations_Controller {
|
|||
unset( $args['s'] );
|
||||
}
|
||||
|
||||
// Retreive variations without specifying a parent product.
|
||||
if ( "/{$this->namespace}/variations" === $request->get_route() ) {
|
||||
unset( $args['post_parent'] );
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
|
|
|
@ -40,18 +40,18 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
* @return array
|
||||
*/
|
||||
protected function prepare_reports_query( $request ) {
|
||||
$args = array();
|
||||
$args['before'] = $request['before'];
|
||||
$args['after'] = $request['after'];
|
||||
$args['interval'] = $request['interval'];
|
||||
$args['page'] = $request['page'];
|
||||
$args['per_page'] = $request['per_page'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['order'] = $request['order'];
|
||||
$args['extended_info'] = $request['extended_info'];
|
||||
$args['categories'] = (array) $request['categories'];
|
||||
$args['status_is'] = (array) $request['status_is'];
|
||||
$args['status_is_not'] = (array) $request['status_is_not'];
|
||||
$args = array();
|
||||
$args['before'] = $request['before'];
|
||||
$args['after'] = $request['after'];
|
||||
$args['interval'] = $request['interval'];
|
||||
$args['page'] = $request['page'];
|
||||
$args['per_page'] = $request['per_page'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['order'] = $request['order'];
|
||||
$args['extended_info'] = $request['extended_info'];
|
||||
$args['category_includes'] = (array) $request['categories'];
|
||||
$args['status_is'] = (array) $request['status_is'];
|
||||
$args['status_is_not'] = (array) $request['status_is_not'];
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
|
|
@ -107,12 +107,6 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
$this->add_order_by_params( $query_args, 'inner', "{$wpdb->wc_category_lookup}.category_tree_id" );
|
||||
}
|
||||
|
||||
// @todo Only products in the category C or orders with products from category C (and, possibly others?).
|
||||
$included_products = $this->get_included_products( $query_args );
|
||||
if ( $included_products ) {
|
||||
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" );
|
||||
}
|
||||
|
||||
$this->add_order_status_clause( $query_args, $order_product_lookup_table, $this->subquery );
|
||||
$this->subquery->add_sql_clause( 'where', "AND {$wpdb->wc_category_lookup}.category_tree_id IS NOT NULL" );
|
||||
}
|
||||
|
@ -167,8 +161,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
* @return string
|
||||
*/
|
||||
protected function get_included_categories_array( $query_args ) {
|
||||
if ( isset( $query_args['categories'] ) && is_array( $query_args['categories'] ) && count( $query_args['categories'] ) > 0 ) {
|
||||
return $query_args['categories'];
|
||||
if ( isset( $query_args['category_includes'] ) && is_array( $query_args['category_includes'] ) && count( $query_args['category_includes'] ) > 0 ) {
|
||||
return $query_args['category_includes'];
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
@ -215,15 +209,15 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
|
||||
// These defaults are only partially applied when used via REST API, as that has its own defaults.
|
||||
$defaults = array(
|
||||
'per_page' => get_option( 'posts_per_page' ),
|
||||
'page' => 1,
|
||||
'order' => 'DESC',
|
||||
'orderby' => 'date',
|
||||
'before' => TimeInterval::default_before(),
|
||||
'after' => TimeInterval::default_after(),
|
||||
'fields' => '*',
|
||||
'categories' => array(),
|
||||
'extended_info' => false,
|
||||
'per_page' => get_option( 'posts_per_page' ),
|
||||
'page' => 1,
|
||||
'order' => 'DESC',
|
||||
'orderby' => 'date',
|
||||
'before' => TimeInterval::default_before(),
|
||||
'after' => TimeInterval::default_after(),
|
||||
'fields' => '*',
|
||||
'category_includes' => array(),
|
||||
'extended_info' => false,
|
||||
);
|
||||
$query_args = wp_parse_args( $query_args, $defaults );
|
||||
$this->normalize_timezones( $query_args, $defaults );
|
||||
|
|
|
@ -93,6 +93,14 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
'slug' => 'products/stats',
|
||||
'description' => __( 'Stats about products.', 'woocommerce-admin' ),
|
||||
),
|
||||
array(
|
||||
'slug' => 'variations',
|
||||
'description' => __( 'Variations detailed reports.', 'woocommerce-admin' ),
|
||||
),
|
||||
array(
|
||||
'slug' => 'variations/stats',
|
||||
'description' => __( 'Stats about variations.', 'woocommerce-admin' ),
|
||||
),
|
||||
array(
|
||||
'slug' => 'categories',
|
||||
'description' => __( 'Product categories detailed reports.', 'woocommerce-admin' ),
|
||||
|
|
|
@ -881,27 +881,21 @@ class DataStore extends SqlQuery {
|
|||
* @return array|stdClass
|
||||
*/
|
||||
protected function get_products_by_cat_ids( $categories ) {
|
||||
$product_categories = get_categories(
|
||||
$terms = get_terms(
|
||||
array(
|
||||
'hide_empty' => 0,
|
||||
'taxonomy' => 'product_cat',
|
||||
'taxonomy' => 'product_cat',
|
||||
'include' => $categories,
|
||||
)
|
||||
);
|
||||
$cat_slugs = array();
|
||||
$categories = array_flip( $categories );
|
||||
foreach ( $product_categories as $product_cat ) {
|
||||
if ( key_exists( $product_cat->cat_ID, $categories ) ) {
|
||||
$cat_slugs[] = $product_cat->slug;
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $cat_slugs ) ) {
|
||||
if ( is_wp_error( $terms ) || empty( $terms ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'category' => $cat_slugs,
|
||||
'category' => wc_list_pluck( $terms, 'slug' ),
|
||||
'limit' => -1,
|
||||
'return' => 'ids',
|
||||
);
|
||||
return wc_get_products( $args );
|
||||
}
|
||||
|
@ -945,18 +939,24 @@ class DataStore extends SqlQuery {
|
|||
$included_products = array();
|
||||
$operator = $this->get_match_operator( $query_args );
|
||||
|
||||
if ( isset( $query_args['categories'] ) && is_array( $query_args['categories'] ) && count( $query_args['categories'] ) > 0 ) {
|
||||
$included_products = $this->get_products_by_cat_ids( $query_args['categories'] );
|
||||
$included_products = empty( $included_products ) ? array( '-1' ) : wc_list_pluck( $included_products, 'get_id' );
|
||||
if ( isset( $query_args['category_includes'] ) && is_array( $query_args['category_includes'] ) && count( $query_args['category_includes'] ) > 0 ) {
|
||||
$included_products = $this->get_products_by_cat_ids( $query_args['category_includes'] );
|
||||
|
||||
// If no products were found in the specified categories, we will force an empty set
|
||||
// by matching a product ID of -1, unless the filters are OR/any and products are specified.
|
||||
if ( empty( $included_products ) ) {
|
||||
$included_products = array( '-1' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $query_args['product_includes'] ) && is_array( $query_args['product_includes'] ) && count( $query_args['product_includes'] ) > 0 ) {
|
||||
if ( count( $included_products ) > 0 ) {
|
||||
if ( 'AND' === $operator ) {
|
||||
// AND results in an intersection between products from selected categories and manually included products.
|
||||
$included_products = array_intersect( $included_products, $query_args['product_includes'] );
|
||||
} elseif ( 'OR' === $operator ) {
|
||||
// Union of products from selected categories and manually included products.
|
||||
$included_products = array_unique( array_merge( $included_products, $query_args['product_includes'] ) );
|
||||
// OR results in a union of products from selected categories and manually included products.
|
||||
$included_products = array_merge( $included_products, $query_args['product_includes'] );
|
||||
}
|
||||
} else {
|
||||
$included_products = $query_args['product_includes'];
|
||||
|
@ -984,11 +984,38 @@ class DataStore extends SqlQuery {
|
|||
* @return string
|
||||
*/
|
||||
protected function get_included_variations( $query_args ) {
|
||||
if ( isset( $query_args['variations'] ) && is_array( $query_args['variations'] ) && count( $query_args['variations'] ) > 0 ) {
|
||||
$query_args['variations'] = array_filter( array_map( 'intval', $query_args['variations'] ) );
|
||||
return $this->get_filtered_ids( $query_args, 'variation_includes' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns comma separated ids of excluded variations, based on query arguments from the user.
|
||||
*
|
||||
* @param array $query_args Parameters supplied by the user.
|
||||
* @return string
|
||||
*/
|
||||
protected function get_excluded_variations( $query_args ) {
|
||||
return $this->get_filtered_ids( $query_args, 'variation_excludes' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ids of disallowed products, based on query arguments from the user.
|
||||
*
|
||||
* @param array $query_args Parameters supplied by the user.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_excluded_products_array( $query_args ) {
|
||||
$excluded_products = array();
|
||||
$operator = $this->get_match_operator( $query_args );
|
||||
|
||||
if ( isset( $query_args['category_excludes'] ) && is_array( $query_args['category_excludes'] ) && count( $query_args['category_excludes'] ) > 0 ) {
|
||||
$excluded_products = $this->get_products_by_cat_ids( $query_args['category_excludes'] );
|
||||
}
|
||||
|
||||
return $this->get_filtered_ids( $query_args, 'variations' );
|
||||
if ( isset( $query_args['product_excludes'] ) && is_array( $query_args['product_excludes'] ) && count( $query_args['product_excludes'] ) > 0 ) {
|
||||
$excluded_products = array_merge( $excluded_products, $query_args['product_excludes'] );
|
||||
}
|
||||
|
||||
return $excluded_products;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -998,7 +1025,8 @@ class DataStore extends SqlQuery {
|
|||
* @return string
|
||||
*/
|
||||
protected function get_excluded_products( $query_args ) {
|
||||
return $this->get_filtered_ids( $query_args, 'product_excludes' );
|
||||
$excluded_products = $this->get_excluded_products_array( $query_args );
|
||||
return implode( ',', $excluded_products );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1008,7 +1036,7 @@ class DataStore extends SqlQuery {
|
|||
* @return string
|
||||
*/
|
||||
protected function get_included_categories( $query_args ) {
|
||||
return $this->get_filtered_ids( $query_args, 'categories' );
|
||||
return $this->get_filtered_ids( $query_args, 'category_includes' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -40,29 +40,31 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
* @return array
|
||||
*/
|
||||
protected function prepare_reports_query( $request ) {
|
||||
$args = array();
|
||||
$args['before'] = $request['before'];
|
||||
$args['after'] = $request['after'];
|
||||
$args['page'] = $request['page'];
|
||||
$args['per_page'] = $request['per_page'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['order'] = $request['order'];
|
||||
$args['product_includes'] = (array) $request['product_includes'];
|
||||
$args['product_excludes'] = (array) $request['product_excludes'];
|
||||
$args['coupon_includes'] = (array) $request['coupon_includes'];
|
||||
$args['coupon_excludes'] = (array) $request['coupon_excludes'];
|
||||
$args['tax_rate_includes'] = (array) $request['tax_rate_includes'];
|
||||
$args['tax_rate_excludes'] = (array) $request['tax_rate_excludes'];
|
||||
$args['status_is'] = (array) $request['status_is'];
|
||||
$args['status_is_not'] = (array) $request['status_is_not'];
|
||||
$args['customer_type'] = $request['customer_type'];
|
||||
$args['extended_info'] = $request['extended_info'];
|
||||
$args['refunds'] = $request['refunds'];
|
||||
$args['match'] = $request['match'];
|
||||
$args['order_includes'] = $request['order_includes'];
|
||||
$args['order_excludes'] = $request['order_excludes'];
|
||||
$args['attribute_is'] = (array) $request['attribute_is'];
|
||||
$args['attribute_is_not'] = (array) $request['attribute_is_not'];
|
||||
$args = array();
|
||||
$args['before'] = $request['before'];
|
||||
$args['after'] = $request['after'];
|
||||
$args['page'] = $request['page'];
|
||||
$args['per_page'] = $request['per_page'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['order'] = $request['order'];
|
||||
$args['product_includes'] = (array) $request['product_includes'];
|
||||
$args['product_excludes'] = (array) $request['product_excludes'];
|
||||
$args['variation_includes'] = (array) $request['variation_includes'];
|
||||
$args['variation_excludes'] = (array) $request['variation_excludes'];
|
||||
$args['coupon_includes'] = (array) $request['coupon_includes'];
|
||||
$args['coupon_excludes'] = (array) $request['coupon_excludes'];
|
||||
$args['tax_rate_includes'] = (array) $request['tax_rate_includes'];
|
||||
$args['tax_rate_excludes'] = (array) $request['tax_rate_excludes'];
|
||||
$args['status_is'] = (array) $request['status_is'];
|
||||
$args['status_is_not'] = (array) $request['status_is_not'];
|
||||
$args['customer_type'] = $request['customer_type'];
|
||||
$args['extended_info'] = $request['extended_info'];
|
||||
$args['refunds'] = $request['refunds'];
|
||||
$args['match'] = $request['match'];
|
||||
$args['order_includes'] = $request['order_includes'];
|
||||
$args['order_excludes'] = $request['order_excludes'];
|
||||
$args['attribute_is'] = (array) $request['attribute_is'];
|
||||
$args['attribute_is_not'] = (array) $request['attribute_is_not'];
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
@ -253,9 +255,9 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
* @return array
|
||||
*/
|
||||
public function get_collection_params() {
|
||||
$params = array();
|
||||
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
|
||||
$params['page'] = array(
|
||||
$params = array();
|
||||
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
|
||||
$params['page'] = array(
|
||||
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
|
||||
'type' => 'integer',
|
||||
'default' => 1,
|
||||
|
@ -263,7 +265,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'minimum' => 1,
|
||||
);
|
||||
$params['per_page'] = array(
|
||||
$params['per_page'] = array(
|
||||
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
|
||||
'type' => 'integer',
|
||||
'default' => 10,
|
||||
|
@ -272,26 +274,26 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'sanitize_callback' => 'absint',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['after'] = array(
|
||||
$params['after'] = array(
|
||||
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['before'] = array(
|
||||
$params['before'] = array(
|
||||
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['order'] = array(
|
||||
$params['order'] = array(
|
||||
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => 'desc',
|
||||
'enum' => array( 'asc', 'desc' ),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['orderby'] = array(
|
||||
$params['orderby'] = array(
|
||||
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => 'date',
|
||||
|
@ -302,7 +304,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['product_includes'] = array(
|
||||
$params['product_includes'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -312,7 +314,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['product_excludes'] = array(
|
||||
$params['product_excludes'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -322,7 +324,27 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['coupon_includes'] = array(
|
||||
$params['variation_includes'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified variation(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
'default' => array(),
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['variation_excludes'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified variation(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
'default' => array(),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['coupon_includes'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -332,7 +354,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['coupon_excludes'] = array(
|
||||
$params['coupon_excludes'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -342,7 +364,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['tax_rate_includes'] = array(
|
||||
$params['tax_rate_includes'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified tax rate(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -352,7 +374,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['tax_rate_excludes'] = array(
|
||||
$params['tax_rate_excludes'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified tax rate(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -362,7 +384,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['status_is'] = array(
|
||||
$params['status_is'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified order status.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_slug_list',
|
||||
|
@ -372,7 +394,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'type' => 'string',
|
||||
),
|
||||
);
|
||||
$params['status_is_not'] = array(
|
||||
$params['status_is_not'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified order status.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_slug_list',
|
||||
|
@ -382,7 +404,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'type' => 'string',
|
||||
),
|
||||
);
|
||||
$params['customer_type'] = array(
|
||||
$params['customer_type'] = array(
|
||||
'description' => __( 'Limit result set to returning or new customers.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
|
@ -393,7 +415,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['refunds'] = array(
|
||||
$params['refunds'] = array(
|
||||
'description' => __( 'Limit result set to specific types of refunds.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
|
@ -406,14 +428,14 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['extended_info'] = array(
|
||||
$params['extended_info'] = array(
|
||||
'description' => __( 'Add additional piece of info about each coupon to the report.', 'woocommerce-admin' ),
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
'sanitize_callback' => 'wc_string_to_bool',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['order_includes'] = array(
|
||||
$params['order_includes'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified order ids.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
|
@ -422,7 +444,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'type' => 'integer',
|
||||
),
|
||||
);
|
||||
$params['order_excludes'] = array(
|
||||
$params['order_excludes'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified order ids.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
|
@ -431,7 +453,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'type' => 'integer',
|
||||
),
|
||||
);
|
||||
$params['attribute_is'] = array(
|
||||
$params['attribute_is'] = array(
|
||||
'description' => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -440,7 +462,7 @@ class Controller extends ReportsController implements ExportableInterface {
|
|||
'default' => array(),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['attribute_is_not'] = array(
|
||||
$params['attribute_is_not'] = array(
|
||||
'description' => __( 'Limit result set to orders that don\'t include products with the specified attributes.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
|
|
@ -89,6 +89,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
$order_tax_lookup_table = $wpdb->prefix . 'wc_order_tax_lookup';
|
||||
$operator = $this->get_match_operator( $query_args );
|
||||
$where_subquery = array();
|
||||
$have_joined_products_table = false;
|
||||
|
||||
$this->add_time_period_sql_params( $query_args, $order_stats_lookup_table );
|
||||
$this->get_limit_sql_params( $query_args );
|
||||
|
@ -139,6 +140,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
$included_products = $this->get_included_products( $query_args );
|
||||
$excluded_products = $this->get_excluded_products( $query_args );
|
||||
if ( $included_products || $excluded_products ) {
|
||||
$have_joined_products_table = true;
|
||||
$this->subquery->add_sql_clause( 'join', "JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id" );
|
||||
}
|
||||
if ( $included_products ) {
|
||||
|
@ -148,6 +150,19 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
$where_subquery[] = "{$order_product_lookup_table}.product_id NOT IN ({$excluded_products})";
|
||||
}
|
||||
|
||||
$included_variations = $this->get_included_variations( $query_args );
|
||||
$excluded_variations = $this->get_excluded_variations( $query_args );
|
||||
if ( ! $have_joined_products_table && ( $included_variations || $excluded_variations ) ) {
|
||||
$have_joined_products_table = true;
|
||||
$this->subquery->add_sql_clause( 'join', "JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id" );
|
||||
}
|
||||
if ( $included_variations ) {
|
||||
$where_subquery[] = "{$order_product_lookup_table}.variation_id IN ({$included_variations})";
|
||||
}
|
||||
if ( $excluded_variations ) {
|
||||
$where_subquery[] = "{$order_product_lookup_table}.variation_id NOT IN ({$excluded_variations})";
|
||||
}
|
||||
|
||||
$included_tax_rates = ! empty( $query_args['tax_rate_includes'] ) ? implode( ',', $query_args['tax_rate_includes'] ) : false;
|
||||
$excluded_tax_rates = ! empty( $query_args['tax_rate_excludes'] ) ? implode( ',', $query_args['tax_rate_excludes'] ) : false;
|
||||
if ( $included_tax_rates || $excluded_tax_rates ) {
|
||||
|
@ -163,7 +178,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
$attribute_subqueries = $this->get_attribute_subqueries( $query_args );
|
||||
if ( $attribute_subqueries['join'] && $attribute_subqueries['where'] ) {
|
||||
// JOIN on product lookup if we haven't already.
|
||||
if ( ! $included_products && ! $excluded_products ) {
|
||||
if ( ! $have_joined_products_table ) {
|
||||
$have_joined_products_table = true;
|
||||
$this->subquery->add_sql_clause( 'join', "JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id" );
|
||||
}
|
||||
// Add JOINs for matching attributes.
|
||||
|
@ -319,11 +335,24 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
$mapped_data[ $product['order_id'] ]['products'] = array();
|
||||
}
|
||||
|
||||
$mapped_data[ $product['order_id'] ]['products'][] = array(
|
||||
'id' => '0' === $product['variation_id'] ? $product['product_id'] : $product['variation_id'],
|
||||
$is_variation = '0' !== $product['variation_id'];
|
||||
$product_data = array(
|
||||
'id' => $is_variation ? $product['variation_id'] : $product['product_id'],
|
||||
'name' => $product['product_name'],
|
||||
'quantity' => $product['product_quantity'],
|
||||
);
|
||||
|
||||
if ( $is_variation ) {
|
||||
$variation = wc_get_product( $product_data['id'] );
|
||||
$separator = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $variation );
|
||||
|
||||
if ( false === strpos( $product_data['name'], $separator ) ) {
|
||||
$attributes = wc_get_formatted_variation( $variation, true, false );
|
||||
$product_data['name'] .= $separator . $attributes;
|
||||
}
|
||||
}
|
||||
|
||||
$mapped_data[ $product['order_id'] ]['products'][] = $product_data;
|
||||
}
|
||||
|
||||
foreach ( $coupons as $coupon ) {
|
||||
|
|
|
@ -39,30 +39,32 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
* @return array
|
||||
*/
|
||||
protected function prepare_reports_query( $request ) {
|
||||
$args = array();
|
||||
$args['before'] = $request['before'];
|
||||
$args['after'] = $request['after'];
|
||||
$args['interval'] = $request['interval'];
|
||||
$args['page'] = $request['page'];
|
||||
$args['per_page'] = $request['per_page'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['order'] = $request['order'];
|
||||
$args['fields'] = $request['fields'];
|
||||
$args['match'] = $request['match'];
|
||||
$args['status_is'] = (array) $request['status_is'];
|
||||
$args['status_is_not'] = (array) $request['status_is_not'];
|
||||
$args['product_includes'] = (array) $request['product_includes'];
|
||||
$args['product_excludes'] = (array) $request['product_excludes'];
|
||||
$args['coupon_includes'] = (array) $request['coupon_includes'];
|
||||
$args['coupon_excludes'] = (array) $request['coupon_excludes'];
|
||||
$args['tax_rate_includes'] = (array) $request['tax_rate_includes'];
|
||||
$args['tax_rate_excludes'] = (array) $request['tax_rate_excludes'];
|
||||
$args['customer'] = $request['customer'];
|
||||
$args['refunds'] = $request['refunds'];
|
||||
$args['attribute_is'] = (array) $request['attribute_is'];
|
||||
$args['attribute_is_not'] = (array) $request['attribute_is_not'];
|
||||
$args['categories'] = (array) $request['categories'];
|
||||
$args['segmentby'] = $request['segmentby'];
|
||||
$args = array();
|
||||
$args['before'] = $request['before'];
|
||||
$args['after'] = $request['after'];
|
||||
$args['interval'] = $request['interval'];
|
||||
$args['page'] = $request['page'];
|
||||
$args['per_page'] = $request['per_page'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['order'] = $request['order'];
|
||||
$args['fields'] = $request['fields'];
|
||||
$args['match'] = $request['match'];
|
||||
$args['status_is'] = (array) $request['status_is'];
|
||||
$args['status_is_not'] = (array) $request['status_is_not'];
|
||||
$args['product_includes'] = (array) $request['product_includes'];
|
||||
$args['product_excludes'] = (array) $request['product_excludes'];
|
||||
$args['variation_includes'] = (array) $request['variation_includes'];
|
||||
$args['variation_excludes'] = (array) $request['variation_excludes'];
|
||||
$args['coupon_includes'] = (array) $request['coupon_includes'];
|
||||
$args['coupon_excludes'] = (array) $request['coupon_excludes'];
|
||||
$args['tax_rate_includes'] = (array) $request['tax_rate_includes'];
|
||||
$args['tax_rate_excludes'] = (array) $request['tax_rate_excludes'];
|
||||
$args['customer'] = $request['customer'];
|
||||
$args['refunds'] = $request['refunds'];
|
||||
$args['attribute_is'] = (array) $request['attribute_is'];
|
||||
$args['attribute_is_not'] = (array) $request['attribute_is_not'];
|
||||
$args['category_includes'] = (array) $request['categories'];
|
||||
$args['segmentby'] = $request['segmentby'];
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
@ -425,7 +427,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
|
||||
);
|
||||
$params['product_excludes'] = array(
|
||||
$params['product_excludes'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -434,7 +436,27 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
'default' => array(),
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['coupon_includes'] = array(
|
||||
$params['variation_includes'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified variation(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
'default' => array(),
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['variation_excludes'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified variation(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
'default' => array(),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['coupon_includes'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -443,7 +465,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
'default' => array(),
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['coupon_excludes'] = array(
|
||||
$params['coupon_excludes'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -452,7 +474,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
'default' => array(),
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['tax_rate_includes'] = array(
|
||||
$params['tax_rate_includes'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified tax rate(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -462,7 +484,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['tax_rate_excludes'] = array(
|
||||
$params['tax_rate_excludes'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified tax rate(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -472,7 +494,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['customer'] = array(
|
||||
$params['customer'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'enum' => array(
|
||||
|
@ -481,7 +503,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['refunds'] = array(
|
||||
$params['refunds'] = array(
|
||||
'description' => __( 'Limit result set to specific types of refunds.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
|
@ -494,7 +516,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['attribute_is'] = array(
|
||||
$params['attribute_is'] = array(
|
||||
'description' => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -503,7 +525,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
'default' => array(),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['attribute_is_not'] = array(
|
||||
$params['attribute_is_not'] = array(
|
||||
'description' => __( 'Limit result set to orders that don\'t include products with the specified attributes.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
|
@ -512,7 +534,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
'default' => array(),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['segmentby'] = array(
|
||||
$params['segmentby'] = array(
|
||||
'description' => __( 'Segment the response by additional constraint.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'enum' => array(
|
||||
|
@ -524,7 +546,7 @@ class Controller extends \Automattic\WooCommerce\Admin\API\Reports\Controller {
|
|||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['fields'] = array(
|
||||
$params['fields'] = array(
|
||||
'description' => __( 'Limit stats fields to the specified items.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_slug_list',
|
||||
|
|
|
@ -142,6 +142,24 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
$this->get_excluded_products( $query_args )
|
||||
);
|
||||
|
||||
// Variations filters.
|
||||
$where_filters[] = $this->get_object_where_filter(
|
||||
$orders_stats_table,
|
||||
'order_id',
|
||||
$product_lookup,
|
||||
'variation_id',
|
||||
'IN',
|
||||
$this->get_included_variations( $query_args )
|
||||
);
|
||||
$where_filters[] = $this->get_object_where_filter(
|
||||
$orders_stats_table,
|
||||
'order_id',
|
||||
$product_lookup,
|
||||
'variation_id',
|
||||
'NOT IN',
|
||||
$this->get_excluded_variations( $query_args )
|
||||
);
|
||||
|
||||
// Coupons filters.
|
||||
$where_filters[] = $this->get_object_where_filter(
|
||||
$orders_stats_table,
|
||||
|
@ -262,7 +280,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
'tax_rate_includes' => array(),
|
||||
'tax_rate_excludes' => array(),
|
||||
'customer' => '',
|
||||
'categories' => array(),
|
||||
'category_includes' => array(),
|
||||
);
|
||||
$query_args = wp_parse_args( $query_args, $defaults );
|
||||
$this->normalize_timezones( $query_args, $defaults );
|
||||
|
|
|
@ -38,7 +38,9 @@ class Controller extends \WC_REST_Reports_Controller implements ExportableInterf
|
|||
* @var array
|
||||
*/
|
||||
protected $param_mapping = array(
|
||||
'products' => 'product_includes',
|
||||
'categories' => 'category_includes',
|
||||
'products' => 'product_includes',
|
||||
'variations' => 'variation_includes',
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -266,16 +266,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
|
||||
// These defaults are only partially applied when used via REST API, as that has its own defaults.
|
||||
$defaults = array(
|
||||
'per_page' => get_option( 'posts_per_page' ),
|
||||
'page' => 1,
|
||||
'order' => 'DESC',
|
||||
'orderby' => 'date',
|
||||
'before' => TimeInterval::default_before(),
|
||||
'after' => TimeInterval::default_after(),
|
||||
'fields' => '*',
|
||||
'categories' => array(),
|
||||
'product_includes' => array(),
|
||||
'extended_info' => false,
|
||||
'per_page' => get_option( 'posts_per_page' ),
|
||||
'page' => 1,
|
||||
'order' => 'DESC',
|
||||
'orderby' => 'date',
|
||||
'before' => TimeInterval::default_before(),
|
||||
'after' => TimeInterval::default_after(),
|
||||
'fields' => '*',
|
||||
'category_includes' => array(),
|
||||
'product_includes' => array(),
|
||||
'extended_info' => false,
|
||||
);
|
||||
$query_args = wp_parse_args( $query_args, $defaults );
|
||||
$this->normalize_timezones( $query_args, $defaults );
|
||||
|
|
|
@ -38,7 +38,9 @@ class Controller extends \WC_REST_Reports_Controller {
|
|||
* @var array
|
||||
*/
|
||||
protected $param_mapping = array(
|
||||
'products' => 'product_includes',
|
||||
'categories' => 'category_includes',
|
||||
'products' => 'product_includes',
|
||||
'variations' => 'variation_includes',
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
@ -106,16 +106,16 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
|
|||
|
||||
// These defaults are only partially applied when used via REST API, as that has its own defaults.
|
||||
$defaults = array(
|
||||
'per_page' => get_option( 'posts_per_page' ),
|
||||
'page' => 1,
|
||||
'order' => 'DESC',
|
||||
'orderby' => 'date',
|
||||
'before' => TimeInterval::default_before(),
|
||||
'after' => TimeInterval::default_after(),
|
||||
'fields' => '*',
|
||||
'categories' => array(),
|
||||
'interval' => 'week',
|
||||
'product_includes' => array(),
|
||||
'per_page' => get_option( 'posts_per_page' ),
|
||||
'page' => 1,
|
||||
'order' => 'DESC',
|
||||
'orderby' => 'date',
|
||||
'before' => TimeInterval::default_before(),
|
||||
'after' => TimeInterval::default_after(),
|
||||
'fields' => '*',
|
||||
'category_includes' => array(),
|
||||
'interval' => 'week',
|
||||
'product_includes' => array(),
|
||||
);
|
||||
$query_args = wp_parse_args( $query_args, $defaults );
|
||||
$this->normalize_timezones( $query_args, $defaults );
|
||||
|
|
|
@ -196,7 +196,7 @@ class Segmenter extends ReportsSegmenter {
|
|||
$segmenting_dimension_name = 'category_id';
|
||||
|
||||
// Restrict our search space for category comparisons.
|
||||
if ( isset( $this->query_args['categories'] ) ) {
|
||||
if ( isset( $this->query_args['category_includes'] ) ) {
|
||||
$category_ids = implode( ',', $this->get_all_segments() );
|
||||
$segmenting_where .= " AND {$wpdb->wc_category_lookup}.category_id IN ( $category_ids )";
|
||||
}
|
||||
|
|
|
@ -344,12 +344,12 @@ class Segmenter {
|
|||
$args['include'] = $this->query_args['product_includes'];
|
||||
}
|
||||
|
||||
if ( isset( $this->query_args['categories'] ) ) {
|
||||
$categories = $this->query_args['categories'];
|
||||
if ( isset( $this->query_args['category_includes'] ) ) {
|
||||
$categories = $this->query_args['category_includes'];
|
||||
$args['category'] = array();
|
||||
foreach ( $categories as $category_id ) {
|
||||
$terms = get_term_by( 'id', $category_id, 'product_cat' );
|
||||
$args['category'] = $terms->slug;
|
||||
$terms = get_term_by( 'id', $category_id, 'product_cat' );
|
||||
$args['category'][] = $terms->slug;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -360,21 +360,21 @@ class Segmenter {
|
|||
$segment_labels[ $id ] = $segment->get_name();
|
||||
}
|
||||
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
|
||||
// @todo Assuming that this will only be used for one product, check assumption.
|
||||
if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
|
||||
$this->all_segment_ids = array();
|
||||
return;
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'return' => 'objects',
|
||||
'limit' => -1,
|
||||
'type' => 'variation',
|
||||
'parent' => $this->query_args['product_includes'][0],
|
||||
);
|
||||
|
||||
if ( isset( $this->query_args['variations'] ) ) {
|
||||
$args['include'] = $this->query_args['variations'];
|
||||
if (
|
||||
isset( $this->query_args['product_includes'] ) &&
|
||||
count( $this->query_args['product_includes'] ) === 1
|
||||
) {
|
||||
$args['parent'] = $this->query_args['product_includes'][0];
|
||||
}
|
||||
|
||||
if ( isset( $this->query_args['variation_includes'] ) ) {
|
||||
$args['include'] = $this->query_args['variation_includes'];
|
||||
}
|
||||
|
||||
$segment_objects = wc_get_products( $args );
|
||||
|
@ -392,8 +392,8 @@ class Segmenter {
|
|||
// If no variations were specified, add a segment for the parent product (variation = 0).
|
||||
// This is to catch simple products with prior sales converted into variable products.
|
||||
// See: https://github.com/woocommerce/woocommerce-admin/issues/2719.
|
||||
if ( empty( $this->query_args['variations'] ) ) {
|
||||
$parent_object = wc_get_product( $this->query_args['product_includes'][0] );
|
||||
if ( isset( $args['parent'] ) && empty( $args['include'] ) ) {
|
||||
$parent_object = wc_get_product( $args['parent'] );
|
||||
$segments[] = 0;
|
||||
$segment_labels[0] = $parent_object->get_name();
|
||||
}
|
||||
|
@ -402,8 +402,8 @@ class Segmenter {
|
|||
'taxonomy' => 'product_cat',
|
||||
);
|
||||
|
||||
if ( isset( $this->query_args['categories'] ) ) {
|
||||
$args['include'] = $this->query_args['categories'];
|
||||
if ( isset( $this->query_args['category_includes'] ) ) {
|
||||
$args['include'] = $this->query_args['category_includes'];
|
||||
}
|
||||
|
||||
// @todo: Look into `wc_get_products` or data store methods and not directly touching the database or post types.
|
||||
|
|
|
@ -9,6 +9,7 @@ namespace Automattic\WooCommerce\Admin\API\Reports\Variations;
|
|||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Controller as ReportsController;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableInterface;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
|
||||
|
||||
|
@ -17,7 +18,7 @@ use \Automattic\WooCommerce\Admin\API\Reports\ExportableTraits;
|
|||
*
|
||||
* @extends WC_REST_Reports_Controller
|
||||
*/
|
||||
class Controller extends \WC_REST_Reports_Controller implements ExportableInterface {
|
||||
class Controller extends ReportsController implements ExportableInterface {
|
||||
/**
|
||||
* Exportable traits.
|
||||
*/
|
||||
|
@ -43,7 +44,7 @@ class Controller extends \WC_REST_Reports_Controller implements ExportableInterf
|
|||
* @var array
|
||||
*/
|
||||
protected $param_mapping = array(
|
||||
'products' => 'product_includes',
|
||||
'variations' => 'variation_includes',
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -252,9 +253,9 @@ class Controller extends \WC_REST_Reports_Controller implements ExportableInterf
|
|||
* @return array
|
||||
*/
|
||||
public function get_collection_params() {
|
||||
$params = array();
|
||||
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
|
||||
$params['page'] = array(
|
||||
$params = array();
|
||||
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
|
||||
$params['page'] = array(
|
||||
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
|
||||
'type' => 'integer',
|
||||
'default' => 1,
|
||||
|
@ -262,7 +263,7 @@ class Controller extends \WC_REST_Reports_Controller implements ExportableInterf
|
|||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'minimum' => 1,
|
||||
);
|
||||
$params['per_page'] = array(
|
||||
$params['per_page'] = array(
|
||||
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
|
||||
'type' => 'integer',
|
||||
'default' => 10,
|
||||
|
@ -271,26 +272,36 @@ class Controller extends \WC_REST_Reports_Controller implements ExportableInterf
|
|||
'sanitize_callback' => 'absint',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['after'] = array(
|
||||
$params['after'] = array(
|
||||
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['before'] = array(
|
||||
$params['before'] = array(
|
||||
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['order'] = array(
|
||||
$params['match'] = array(
|
||||
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => 'all',
|
||||
'enum' => array(
|
||||
'all',
|
||||
'any',
|
||||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['order'] = array(
|
||||
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => 'desc',
|
||||
'enum' => array( 'asc', 'desc' ),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['orderby'] = array(
|
||||
$params['orderby'] = array(
|
||||
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => 'date',
|
||||
|
@ -303,16 +314,27 @@ class Controller extends \WC_REST_Reports_Controller implements ExportableInterf
|
|||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['products'] = array(
|
||||
'description' => __( 'Limit result to items with specified product ids.', 'woocommerce-admin' ),
|
||||
$params['product_includes'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified parent product(s).', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
'default' => array(),
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['variations'] = array(
|
||||
$params['product_excludes'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified parent product(s).', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
'default' => array(),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['variations'] = array(
|
||||
'description' => __( 'Limit result to items with specified variation ids.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
|
@ -321,13 +343,49 @@ class Controller extends \WC_REST_Reports_Controller implements ExportableInterf
|
|||
'type' => 'integer',
|
||||
),
|
||||
);
|
||||
$params['extended_info'] = array(
|
||||
'description' => __( 'Add additional piece of info about each product to the report.', 'woocommerce-admin' ),
|
||||
$params['extended_info'] = array(
|
||||
'description' => __( 'Add additional piece of info about each variation to the report.', 'woocommerce-admin' ),
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
'sanitize_callback' => 'wc_string_to_bool',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['attribute_is'] = array(
|
||||
'description' => __( 'Limit result set to variations that include the specified attributes.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'array',
|
||||
),
|
||||
'default' => array(),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['attribute_is_not'] = array(
|
||||
'description' => __( 'Limit result set to variations that don\'t include the specified attributes.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'array',
|
||||
),
|
||||
'default' => array(),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['category_includes'] = array(
|
||||
'description' => __( 'Limit result set to variations in the specified categories.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
);
|
||||
$params['category_excludes'] = array(
|
||||
'description' => __( 'Limit result set to variations not in the specified categories.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
}
|
||||
|
||||
$table_name = self::get_db_table_name();
|
||||
$join = "JOIN {$wpdb->postmeta} AS postmeta ON {$table_name}.variation_id = postmeta.post_id AND postmeta.meta_key = '_sku'";
|
||||
$join = "LEFT JOIN {$wpdb->postmeta} AS postmeta ON {$table_name}.variation_id = postmeta.post_id AND postmeta.meta_key = '_sku'";
|
||||
|
||||
if ( 'inner' === $arg_name ) {
|
||||
$this->subquery->add_sql_clause( 'join', $join );
|
||||
|
@ -119,12 +119,15 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
protected function add_sql_query_params( $query_args ) {
|
||||
global $wpdb;
|
||||
$order_product_lookup_table = self::get_db_table_name();
|
||||
$order_stats_lookup_table = $wpdb->prefix . 'wc_order_stats';
|
||||
$where_subquery = array();
|
||||
|
||||
$this->add_time_period_sql_params( $query_args, $order_product_lookup_table );
|
||||
$this->get_limit_sql_params( $query_args );
|
||||
$this->add_order_by_sql_params( $query_args );
|
||||
|
||||
if ( count( $query_args['variations'] ) > 0 ) {
|
||||
$included_variations = $this->get_included_variations( $query_args );
|
||||
if ( $included_variations > 0 ) {
|
||||
$this->add_from_sql_params( $query_args, 'outer' );
|
||||
} else {
|
||||
$this->add_from_sql_params( $query_args, 'inner' );
|
||||
|
@ -135,16 +138,41 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" );
|
||||
}
|
||||
|
||||
if ( count( $query_args['variations'] ) > 0 ) {
|
||||
$allowed_variations_str = self::get_filtered_ids( $query_args, 'variations' );
|
||||
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id IN ({$allowed_variations_str})" );
|
||||
$excluded_products = $this->get_excluded_products( $query_args );
|
||||
if ( $excluded_products ) {
|
||||
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id NOT IN ({$excluded_products})" );
|
||||
}
|
||||
|
||||
if ( $included_variations ) {
|
||||
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id IN ({$included_variations})" );
|
||||
} elseif ( ! $included_products ) {
|
||||
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id != 0" );
|
||||
}
|
||||
|
||||
$order_status_filter = $this->get_status_subquery( $query_args );
|
||||
if ( $order_status_filter ) {
|
||||
$this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id" );
|
||||
$this->subquery->add_sql_clause( 'join', "JOIN {$order_stats_lookup_table} ON {$order_product_lookup_table}.order_id = {$order_stats_lookup_table}.order_id" );
|
||||
$this->subquery->add_sql_clause( 'where', "AND ( {$order_status_filter} )" );
|
||||
}
|
||||
|
||||
$attribute_subqueries = $this->get_attribute_subqueries( $query_args );
|
||||
if ( $attribute_subqueries['join'] && $attribute_subqueries['where'] ) {
|
||||
// JOIN on product lookup if we haven't already.
|
||||
if ( ! $order_status_filter ) {
|
||||
$this->subquery->add_sql_clause( 'join', "JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id" );
|
||||
}
|
||||
// Add JOINs for matching attributes.
|
||||
foreach ( $attribute_subqueries['join'] as $attribute_join ) {
|
||||
$this->subquery->add_sql_clause( 'join', $attribute_join );
|
||||
}
|
||||
// Add WHEREs for matching attributes.
|
||||
$where_subquery = array_merge( $where_subquery, $attribute_subqueries['where'] );
|
||||
}
|
||||
|
||||
if ( 0 < count( $where_subquery ) ) {
|
||||
$operator = $this->get_match_operator( $query_args );
|
||||
$this->subquery->add_sql_clause( 'where', 'AND (' . implode( " {$operator} ", $where_subquery ) . ')' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -176,26 +204,32 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
$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'] );
|
||||
$variations = array();
|
||||
$parent_product = wc_get_product( $product_data['product_id'] );
|
||||
$attributes = array();
|
||||
|
||||
// Base extended info off the parent variable product if the variation ID is 0.
|
||||
// This is caused by simple products with prior sales being converted into variable products.
|
||||
// See: https://github.com/woocommerce/woocommerce-admin/issues/2719.
|
||||
$variation_id = (int) $product_data['variation_id'];
|
||||
$variation_product = ( 0 === $variation_id ) ? $product : wc_get_product( $variation_id );
|
||||
$attributes = array();
|
||||
$variation_product = ( 0 === $variation_id ) ? $parent_product : wc_get_product( $variation_id );
|
||||
|
||||
// Fall back to the parent product if the variation can't be found.
|
||||
$extended_attributes_product = is_a( $variation_product, 'WC_Product' ) ? $variation_product : $parent_product;
|
||||
|
||||
foreach ( $extended_attributes as $extended_attribute ) {
|
||||
$function = 'get_' . $extended_attribute;
|
||||
if ( is_callable( array( $variation_product, $function ) ) ) {
|
||||
$value = $variation_product->{$function}();
|
||||
if ( is_callable( array( $extended_attributes_product, $function ) ) ) {
|
||||
$value = $extended_attributes_product->{$function}();
|
||||
$extended_info[ $extended_attribute ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a variation, add its attributes.
|
||||
if ( 0 < $variation_id ) {
|
||||
// NOTE: We don't fall back to the parent product here because it will include all possible attribute options.
|
||||
if (
|
||||
0 < $variation_id &&
|
||||
is_callable( array( $variation_product, 'get_variation_attributes' ) )
|
||||
) {
|
||||
$variation_attributes = $variation_product->get_variation_attributes();
|
||||
|
||||
foreach ( $variation_attributes as $attribute_name => $attribute ) {
|
||||
|
@ -235,16 +269,16 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
|
||||
// These defaults are only partially applied when used via REST API, as that has its own defaults.
|
||||
$defaults = array(
|
||||
'per_page' => get_option( 'posts_per_page' ),
|
||||
'page' => 1,
|
||||
'order' => 'DESC',
|
||||
'orderby' => 'date',
|
||||
'before' => TimeInterval::default_before(),
|
||||
'after' => TimeInterval::default_after(),
|
||||
'fields' => '*',
|
||||
'products' => array(),
|
||||
'variations' => array(),
|
||||
'extended_info' => false,
|
||||
'per_page' => get_option( 'posts_per_page' ),
|
||||
'page' => 1,
|
||||
'order' => 'DESC',
|
||||
'orderby' => 'date',
|
||||
'before' => TimeInterval::default_before(),
|
||||
'after' => TimeInterval::default_after(),
|
||||
'fields' => '*',
|
||||
'product_includes' => array(),
|
||||
'variation_includes' => array(),
|
||||
'extended_info' => false,
|
||||
);
|
||||
$query_args = wp_parse_args( $query_args, $defaults );
|
||||
$this->normalize_timezones( $query_args, $defaults );
|
||||
|
@ -266,22 +300,28 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
'page_no' => 0,
|
||||
);
|
||||
|
||||
$included_products = $this->get_included_products_array( $query_args );
|
||||
|
||||
$selections = $this->selected_columns( $query_args );
|
||||
$included_variations =
|
||||
( isset( $query_args['variation_includes'] ) && is_array( $query_args['variation_includes'] ) )
|
||||
? $query_args['variation_includes']
|
||||
: array();
|
||||
$params = $this->get_limit_params( $query_args );
|
||||
$this->add_sql_query_params( $query_args );
|
||||
$params = $this->get_limit_params( $query_args );
|
||||
if ( count( $included_products ) > 0 && count( $query_args['variations'] ) > 0 ) {
|
||||
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
|
||||
|
||||
if ( count( $included_variations ) > 0 ) {
|
||||
$total_results = count( $included_variations );
|
||||
$total_pages = (int) ceil( $total_results / $params['per_page'] );
|
||||
|
||||
$this->subquery->clear_sql_clause( 'select' );
|
||||
$this->subquery->add_sql_clause( 'select', $selections );
|
||||
|
||||
if ( 'date' === $query_args['orderby'] ) {
|
||||
$this->subquery->add_sql_clause( 'select', ", {$table_name}.date_created" );
|
||||
}
|
||||
|
||||
$total_results = count( $query_args['variations'] );
|
||||
$total_pages = (int) ceil( $total_results / $params['per_page'] );
|
||||
|
||||
$fields = $this->get_fields( $query_args );
|
||||
$join_selections = $this->format_join_selections( $fields, array( 'product_id', 'variation_id' ) );
|
||||
$ids_table = $this->get_ids_table( $query_args['variations'], 'variation_id', array( 'product_id' => $included_products[0] ) );
|
||||
$join_selections = $this->format_join_selections( $fields, array( 'variation_id' ) );
|
||||
$ids_table = $this->get_ids_table( $included_variations, 'variation_id' );
|
||||
|
||||
$this->add_sql_clause( 'select', $join_selections );
|
||||
$this->add_sql_clause( 'from', '(' );
|
||||
|
@ -309,8 +349,9 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
|||
}
|
||||
|
||||
$this->subquery->clear_sql_clause( 'select' );
|
||||
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
|
||||
$this->subquery->add_sql_clause( 'select', $selections );
|
||||
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
|
||||
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
|
||||
$variations_query = $this->subquery->get_query_statement();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,474 @@
|
|||
<?php
|
||||
/**
|
||||
* REST API Reports variations stats controller
|
||||
*
|
||||
* Handles requests to the /reports/variations/stats endpoint.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;
|
||||
|
||||
/**
|
||||
* REST API Reports variations stats controller class.
|
||||
*
|
||||
* @extends WC_REST_Reports_Controller
|
||||
*/
|
||||
class Controller extends \WC_REST_Reports_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc-analytics';
|
||||
|
||||
/**
|
||||
* Route base.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'reports/variations/stats';
|
||||
|
||||
/**
|
||||
* Mapping between external parameter name and name used in query class.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $param_mapping = array(
|
||||
'variations' => 'variation_includes',
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter( 'woocommerce_analytics_variations_stats_select_query', array( $this, 'set_default_report_data' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all reports.
|
||||
*
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_items( $request ) {
|
||||
$query_args = array(
|
||||
'fields' => array(
|
||||
'items_sold',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'variations_count',
|
||||
),
|
||||
);
|
||||
|
||||
$registered = array_keys( $this->get_collection_params() );
|
||||
foreach ( $registered as $param_name ) {
|
||||
if ( isset( $request[ $param_name ] ) ) {
|
||||
if ( isset( $this->param_mapping[ $param_name ] ) ) {
|
||||
$query_args[ $this->param_mapping[ $param_name ] ] = $request[ $param_name ];
|
||||
} else {
|
||||
$query_args[ $param_name ] = $request[ $param_name ];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$query = new Query( $query_args );
|
||||
try {
|
||||
$report_data = $query->get_data();
|
||||
} catch ( ParameterException $e ) {
|
||||
return new \WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
|
||||
}
|
||||
|
||||
$out_data = array(
|
||||
'totals' => get_object_vars( $report_data->totals ),
|
||||
'intervals' => array(),
|
||||
);
|
||||
|
||||
foreach ( $report_data->intervals as $interval_data ) {
|
||||
$item = $this->prepare_item_for_response( $interval_data, $request );
|
||||
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
|
||||
}
|
||||
|
||||
$response = rest_ensure_response( $out_data );
|
||||
$response->header( 'X-WP-Total', (int) $report_data->total );
|
||||
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
|
||||
|
||||
$page = $report_data->page_no;
|
||||
$max_pages = $report_data->pages;
|
||||
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
|
||||
if ( $page > 1 ) {
|
||||
$prev_page = $page - 1;
|
||||
if ( $prev_page > $max_pages ) {
|
||||
$prev_page = $max_pages;
|
||||
}
|
||||
$prev_link = add_query_arg( 'page', $prev_page, $base );
|
||||
$response->link_header( 'prev', $prev_link );
|
||||
}
|
||||
if ( $max_pages > $page ) {
|
||||
$next_page = $page + 1;
|
||||
$next_link = add_query_arg( 'page', $next_page, $base );
|
||||
$response->link_header( 'next', $next_link );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a report object for serialization.
|
||||
*
|
||||
* @param Array $report Report data.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function prepare_item_for_response( $report, $request ) {
|
||||
$data = $report;
|
||||
|
||||
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
|
||||
$data = $this->add_additional_fields_to_object( $data, $request );
|
||||
$data = $this->filter_response_by_context( $data, $context );
|
||||
|
||||
// Wrap the data in a response object.
|
||||
$response = rest_ensure_response( $data );
|
||||
|
||||
/**
|
||||
* Filter a report returned from the API.
|
||||
*
|
||||
* Allows modification of the report data right before it is returned.
|
||||
*
|
||||
* @param WP_REST_Response $response The response object.
|
||||
* @param object $report The original report object.
|
||||
* @param WP_REST_Request $request Request used to generate the response.
|
||||
*/
|
||||
return apply_filters( 'woocommerce_rest_prepare_report_variations_stats', $response, $report, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Report's schema, conforming to JSON Schema.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
$data_values = array(
|
||||
'items_sold' => array(
|
||||
'title' => __( 'Items Sold', 'woocommerce-admin' ),
|
||||
'description' => __( 'Number of items sold.', 'woocommerce-admin' ),
|
||||
'type' => 'integer',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'indicator' => true,
|
||||
),
|
||||
'net_revenue' => array(
|
||||
'description' => __( 'Net Sales.', 'woocommerce-admin' ),
|
||||
'type' => 'number',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'format' => 'currency',
|
||||
),
|
||||
'orders_count' => array(
|
||||
'description' => __( 'Number of orders.', 'woocommerce-admin' ),
|
||||
'type' => 'integer',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
);
|
||||
|
||||
$segments = array(
|
||||
'segments' => array(
|
||||
'description' => __( 'Reports data grouped by segment condition.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'segment_id' => array(
|
||||
'description' => __( 'Segment identificator.', 'woocommerce-admin' ),
|
||||
'type' => 'integer',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'segment_label' => array(
|
||||
'description' => __( 'Human readable segment label, either product or variation name.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'enum' => array( 'day', 'week', 'month', 'year' ),
|
||||
),
|
||||
'subtotals' => array(
|
||||
'description' => __( 'Interval subtotals.', 'woocommerce-admin' ),
|
||||
'type' => 'object',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'properties' => $data_values,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$totals = array_merge( $data_values, $segments );
|
||||
|
||||
$schema = array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'report_variations_stats',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'totals' => array(
|
||||
'description' => __( 'Totals data.', 'woocommerce-admin' ),
|
||||
'type' => 'object',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'properties' => $totals,
|
||||
),
|
||||
'intervals' => array(
|
||||
'description' => __( 'Reports data grouped by intervals.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'interval' => array(
|
||||
'description' => __( 'Type of interval.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'enum' => array( 'day', 'week', 'month', 'year' ),
|
||||
),
|
||||
'date_start' => array(
|
||||
'description' => __( "The date the report start, in the site's timezone.", 'woocommerce-admin' ),
|
||||
'type' => 'date-time',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'date_start_gmt' => array(
|
||||
'description' => __( 'The date the report start, as GMT.', 'woocommerce-admin' ),
|
||||
'type' => 'date-time',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'date_end' => array(
|
||||
'description' => __( "The date the report end, in the site's timezone.", 'woocommerce-admin' ),
|
||||
'type' => 'date-time',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'date_end_gmt' => array(
|
||||
'description' => __( 'The date the report end, as GMT.', 'woocommerce-admin' ),
|
||||
'type' => 'date-time',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'subtotals' => array(
|
||||
'description' => __( 'Interval subtotals.', 'woocommerce-admin' ),
|
||||
'type' => 'object',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'properties' => $totals,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $this->add_additional_fields_schema( $schema );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default results to 0 if API returns an empty array
|
||||
*
|
||||
* @param Mixed $results Report data.
|
||||
* @return object
|
||||
*/
|
||||
public function set_default_report_data( $results ) {
|
||||
if ( empty( $results ) ) {
|
||||
$results = new \stdClass();
|
||||
$results->total = 0;
|
||||
$results->totals = new \stdClass();
|
||||
$results->totals->items_sold = 0;
|
||||
$results->totals->net_revenue = 0;
|
||||
$results->totals->orders_count = 0;
|
||||
$results->intervals = array();
|
||||
$results->pages = 1;
|
||||
$results->page_no = 1;
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query params for collections.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_collection_params() {
|
||||
$params = array();
|
||||
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
|
||||
$params['page'] = array(
|
||||
'description' => __( 'Current page of the collection.', 'woocommerce-admin' ),
|
||||
'type' => 'integer',
|
||||
'default' => 1,
|
||||
'sanitize_callback' => 'absint',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'minimum' => 1,
|
||||
);
|
||||
$params['per_page'] = array(
|
||||
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce-admin' ),
|
||||
'type' => 'integer',
|
||||
'default' => 10,
|
||||
'minimum' => 1,
|
||||
'maximum' => 100,
|
||||
'sanitize_callback' => 'absint',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['after'] = array(
|
||||
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['before'] = array(
|
||||
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['match'] = array(
|
||||
'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => 'all',
|
||||
'enum' => array(
|
||||
'all',
|
||||
'any',
|
||||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['order'] = array(
|
||||
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => 'desc',
|
||||
'enum' => array( 'asc', 'desc' ),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['orderby'] = array(
|
||||
'description' => __( 'Sort collection by object attribute.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => 'date',
|
||||
'enum' => array(
|
||||
'date',
|
||||
'net_revenue',
|
||||
'coupons',
|
||||
'refunds',
|
||||
'shipping',
|
||||
'taxes',
|
||||
'net_revenue',
|
||||
'orders_count',
|
||||
'items_sold',
|
||||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['interval'] = array(
|
||||
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'default' => 'week',
|
||||
'enum' => array(
|
||||
'hour',
|
||||
'day',
|
||||
'week',
|
||||
'month',
|
||||
'quarter',
|
||||
'year',
|
||||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['category_includes'] = array(
|
||||
'description' => __( 'Limit result to items from the specified categories.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
);
|
||||
$params['category_excludes'] = array(
|
||||
'description' => __( 'Limit result set to variations not in the specified categories.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
);
|
||||
$params['product_includes'] = array(
|
||||
'description' => __( 'Limit result set to items that have the specified parent product(s).', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
'default' => array(),
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['product_excludes'] = array(
|
||||
'description' => __( 'Limit result set to items that don\'t have the specified parent product(s).', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
'default' => array(),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
);
|
||||
$params['variations'] = array(
|
||||
'description' => __( 'Limit result to items with specified variation ids.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
);
|
||||
$params['segmentby'] = array(
|
||||
'description' => __( 'Segment the response by additional constraint.', 'woocommerce-admin' ),
|
||||
'type' => 'string',
|
||||
'enum' => array(
|
||||
'product',
|
||||
'category',
|
||||
'variation',
|
||||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['fields'] = array(
|
||||
'description' => __( 'Limit stats fields to the specified items.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_slug_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
);
|
||||
$params['attribute_is'] = array(
|
||||
'description' => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'array',
|
||||
),
|
||||
'default' => array(),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['attribute_is_not'] = array(
|
||||
'description' => __( 'Limit result set to orders that don\'t include products with the specified attributes.', 'woocommerce-admin' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'array',
|
||||
),
|
||||
'default' => array(),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
<?php
|
||||
/**
|
||||
* API\Reports\Products\Stats\DataStore class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Variations\DataStore as VariationsDataStore;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\TimeInterval;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
|
||||
|
||||
/**
|
||||
* API\Reports\Variations\Stats\DataStore.
|
||||
*/
|
||||
class DataStore extends VariationsDataStore implements DataStoreInterface {
|
||||
|
||||
/**
|
||||
* Mapping columns to data type to return correct response types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $column_types = array(
|
||||
'items_sold' => 'intval',
|
||||
'net_revenue' => 'floatval',
|
||||
'orders_count' => 'intval',
|
||||
'variations_count' => 'intval',
|
||||
);
|
||||
|
||||
/**
|
||||
* Data store context used to pass to filters.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $context = 'variatons_stats';
|
||||
|
||||
/**
|
||||
* Assign report columns once full table name has been assigned.
|
||||
*/
|
||||
protected function assign_report_columns() {
|
||||
$table_name = self::get_db_table_name();
|
||||
$this->report_columns = array(
|
||||
'items_sold' => 'SUM(product_qty) as items_sold',
|
||||
'net_revenue' => 'SUM(product_net_revenue) AS net_revenue',
|
||||
'orders_count' => "COUNT( DISTINCT ( CASE WHEN product_gross_revenue >= 0 THEN {$table_name}.order_id END ) ) as orders_count",
|
||||
'variations_count' => 'COUNT(DISTINCT variation_id) as variations_count',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the database query with parameters used for Products Stats report: categories and order status.
|
||||
*
|
||||
* @param array $query_args Query arguments supplied by the user.
|
||||
*/
|
||||
protected function update_sql_query_params( $query_args ) {
|
||||
global $wpdb;
|
||||
|
||||
$products_where_clause = '';
|
||||
$products_from_clause = '';
|
||||
$where_subquery = array();
|
||||
$order_product_lookup_table = self::get_db_table_name();
|
||||
|
||||
$included_products = $this->get_included_products( $query_args );
|
||||
if ( $included_products ) {
|
||||
$products_where_clause .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})";
|
||||
}
|
||||
|
||||
$excluded_products = $this->get_excluded_products( $query_args );
|
||||
if ( $excluded_products ) {
|
||||
$products_where_clause .= "AND {$order_product_lookup_table}.product_id NOT IN ({$excluded_products})";
|
||||
}
|
||||
|
||||
$included_variations = $this->get_included_variations( $query_args );
|
||||
if ( $included_variations ) {
|
||||
$products_where_clause .= " AND {$order_product_lookup_table}.variation_id IN ({$included_variations})";
|
||||
} else {
|
||||
$products_where_clause .= " AND {$order_product_lookup_table}.variation_id != 0";
|
||||
}
|
||||
|
||||
$order_status_filter = $this->get_status_subquery( $query_args );
|
||||
if ( $order_status_filter ) {
|
||||
$products_from_clause .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
|
||||
$products_where_clause .= " AND ( {$order_status_filter} )";
|
||||
}
|
||||
|
||||
$attribute_subqueries = $this->get_attribute_subqueries( $query_args );
|
||||
if ( $attribute_subqueries['join'] && $attribute_subqueries['where'] ) {
|
||||
// JOIN on product lookup if we haven't already.
|
||||
if ( ! $order_status_filter ) {
|
||||
$products_from_clause .= " JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id";
|
||||
}
|
||||
// Add JOINs for matching attributes.
|
||||
foreach ( $attribute_subqueries['join'] as $attribute_join ) {
|
||||
$products_from_clause .= ' ' . $attribute_join;
|
||||
}
|
||||
// Add WHEREs for matching attributes.
|
||||
$where_subquery = array_merge( $where_subquery, $attribute_subqueries['where'] );
|
||||
}
|
||||
|
||||
if ( 0 < count( $where_subquery ) ) {
|
||||
$operator = $this->get_match_operator( $query_args );
|
||||
$products_where_clause .= 'AND (' . implode( " {$operator} ", $where_subquery ) . ')';
|
||||
}
|
||||
|
||||
$this->add_time_period_sql_params( $query_args, $order_product_lookup_table );
|
||||
$this->total_query->add_sql_clause( 'where', $products_where_clause );
|
||||
$this->total_query->add_sql_clause( 'join', $products_from_clause );
|
||||
|
||||
$this->add_intervals_sql_params( $query_args, $order_product_lookup_table );
|
||||
$this->interval_query->add_sql_clause( 'where', $products_where_clause );
|
||||
$this->interval_query->add_sql_clause( 'join', $products_from_clause );
|
||||
$this->interval_query->add_sql_clause( 'select', $this->get_sql_clause( 'select' ) . ' AS time_interval' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the report data based on parameters supplied by the user.
|
||||
*
|
||||
* @since 3.5.0
|
||||
* @param array $query_args Query parameters.
|
||||
* @return stdClass|WP_Error Data.
|
||||
*/
|
||||
public function get_data( $query_args ) {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::get_db_table_name();
|
||||
|
||||
// These defaults are only partially applied when used via REST API, as that has its own defaults.
|
||||
$defaults = array(
|
||||
'per_page' => get_option( 'posts_per_page' ),
|
||||
'page' => 1,
|
||||
'order' => 'DESC',
|
||||
'orderby' => 'date',
|
||||
'before' => TimeInterval::default_before(),
|
||||
'after' => TimeInterval::default_after(),
|
||||
'fields' => '*',
|
||||
'category_includes' => array(),
|
||||
'interval' => 'week',
|
||||
'product_includes' => array(),
|
||||
'variation_includes' => array(),
|
||||
);
|
||||
$query_args = wp_parse_args( $query_args, $defaults );
|
||||
$this->normalize_timezones( $query_args, $defaults );
|
||||
|
||||
/*
|
||||
* We need to get the cache key here because
|
||||
* parent::update_intervals_sql_params() modifies $query_args.
|
||||
*/
|
||||
$cache_key = $this->get_cache_key( $query_args );
|
||||
$data = $this->get_cached_data( $cache_key );
|
||||
|
||||
if ( false === $data ) {
|
||||
$this->initialize_queries();
|
||||
|
||||
$selections = $this->selected_columns( $query_args );
|
||||
$params = $this->get_limit_params( $query_args );
|
||||
|
||||
$this->update_sql_query_params( $query_args );
|
||||
$this->get_limit_sql_params( $query_args );
|
||||
$this->interval_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
|
||||
|
||||
$db_intervals = $wpdb->get_col(
|
||||
$this->interval_query->get_query_statement()
|
||||
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
|
||||
|
||||
$db_interval_count = count( $db_intervals );
|
||||
$expected_interval_count = TimeInterval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
|
||||
$total_pages = (int) ceil( $expected_interval_count / $params['per_page'] );
|
||||
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$intervals = array();
|
||||
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
|
||||
$this->total_query->add_sql_clause( 'select', $selections );
|
||||
$this->total_query->add_sql_clause( 'where_time', $this->get_sql_clause( 'where_time' ) );
|
||||
|
||||
$totals = $wpdb->get_results(
|
||||
$this->total_query->get_query_statement(),
|
||||
ARRAY_A
|
||||
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
|
||||
|
||||
// @todo remove these assignements when refactoring segmenter classes to use query objects.
|
||||
$totals_query = array(
|
||||
'from_clause' => $this->total_query->get_sql_clause( 'join' ),
|
||||
'where_time_clause' => $this->total_query->get_sql_clause( 'where_time' ),
|
||||
'where_clause' => $this->total_query->get_sql_clause( 'where' ),
|
||||
);
|
||||
$intervals_query = array(
|
||||
'select_clause' => $this->get_sql_clause( 'select' ),
|
||||
'from_clause' => $this->interval_query->get_sql_clause( 'join' ),
|
||||
'where_time_clause' => $this->interval_query->get_sql_clause( 'where_time' ),
|
||||
'where_clause' => $this->interval_query->get_sql_clause( 'where' ),
|
||||
'order_by' => $this->get_sql_clause( 'order_by' ),
|
||||
'limit' => $this->get_sql_clause( 'limit' ),
|
||||
);
|
||||
$segmenter = new Segmenter( $query_args, $this->report_columns );
|
||||
$totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name );
|
||||
|
||||
if ( null === $totals ) {
|
||||
return new \WP_Error( 'woocommerce_analytics_variations_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce-admin' ) );
|
||||
}
|
||||
|
||||
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
|
||||
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
|
||||
$this->interval_query->add_sql_clause( 'select', ", MAX(${table_name}.date_created) AS datetime_anchor" );
|
||||
if ( '' !== $selections ) {
|
||||
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
|
||||
}
|
||||
|
||||
$intervals = $wpdb->get_results(
|
||||
$this->interval_query->get_query_statement(),
|
||||
ARRAY_A
|
||||
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
|
||||
|
||||
if ( null === $intervals ) {
|
||||
return new \WP_Error( 'woocommerce_analytics_variations_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce-admin' ) );
|
||||
}
|
||||
|
||||
$totals = (object) $this->cast_numbers( $totals[0] );
|
||||
|
||||
$data = (object) array(
|
||||
'totals' => $totals,
|
||||
'intervals' => $intervals,
|
||||
'total' => $expected_interval_count,
|
||||
'pages' => $total_pages,
|
||||
'page_no' => (int) $query_args['page'],
|
||||
);
|
||||
|
||||
if ( TimeInterval::intervals_missing( $expected_interval_count, $db_interval_count, $params['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
|
||||
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
|
||||
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
|
||||
$this->remove_extra_records( $data, $query_args['page'], $params['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'], $query_args['order'] );
|
||||
} else {
|
||||
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals );
|
||||
}
|
||||
$segmenter->add_intervals_segments( $data, $intervals_query, $table_name );
|
||||
$this->create_interval_subtotals( $data->intervals );
|
||||
|
||||
wp_cache_set( $cache_key, $data, $this->cache_group );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes order_by clause to match to SQL query.
|
||||
*
|
||||
* @param string $order_by Order by option requeste by user.
|
||||
* @return string
|
||||
*/
|
||||
protected function normalize_order_by( $order_by ) {
|
||||
if ( 'date' === $order_by ) {
|
||||
return 'time_interval';
|
||||
}
|
||||
|
||||
return $order_by;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize query objects.
|
||||
*/
|
||||
protected function initialize_queries() {
|
||||
$this->clear_all_clauses();
|
||||
unset( $this->subquery );
|
||||
$this->total_query = new SqlQuery( $this->context . '_total' );
|
||||
$this->total_query->add_sql_clause( 'from', self::get_db_table_name() );
|
||||
|
||||
$this->interval_query = new SqlQuery( $this->context . '_interval' );
|
||||
$this->interval_query->add_sql_clause( 'from', self::get_db_table_name() );
|
||||
$this->interval_query->add_sql_clause( 'group_by', 'time_interval' );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
/**
|
||||
* Class for parameter-based Variations Stats Report querying
|
||||
*
|
||||
* Example usage:
|
||||
* $args = array(
|
||||
* 'before' => '2018-07-19 00:00:00',
|
||||
* 'after' => '2018-07-05 00:00:00',
|
||||
* 'page' => 2,
|
||||
* 'categories' => array(15, 18),
|
||||
* 'product_ids' => array(1,2,3)
|
||||
* );
|
||||
* $report = new \Automattic\WooCommerce\Admin\API\Reports\Variations\Stats\Query( $args );
|
||||
* $mydata = $report->get_data();
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Query as ReportsQuery;
|
||||
|
||||
/**
|
||||
* API\Reports\Variations\Stats\Query
|
||||
*/
|
||||
class Query extends ReportsQuery {
|
||||
|
||||
/**
|
||||
* Valid fields for Products report.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_default_query_vars() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get variations data based on the current query vars.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_data() {
|
||||
$args = apply_filters( 'woocommerce_analytics_variations_stats_query_args', $this->get_query_vars() );
|
||||
|
||||
$data_store = \WC_Data_Store::load( 'report-variations-stats' );
|
||||
$results = $data_store->get_data( $args );
|
||||
return apply_filters( 'woocommerce_analytics_variations_stats_select_query', $results, $args );
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
<?php
|
||||
/**
|
||||
* Class for adding segmenting support without cluttering the data stores.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\API\Reports\Variations\Stats;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\Segmenter as ReportsSegmenter;
|
||||
use \Automattic\WooCommerce\Admin\API\Reports\ParameterException;
|
||||
|
||||
/**
|
||||
* Date & time interval and numeric range handling class for Reporting API.
|
||||
*/
|
||||
class Segmenter extends ReportsSegmenter {
|
||||
|
||||
/**
|
||||
* Returns column => query mapping to be used for product-related product-level segmenting query
|
||||
* (e.g. products sold, revenue from product X when segmenting by category).
|
||||
*
|
||||
* @param string $products_table Name of SQL table containing the product-level segmenting info.
|
||||
*
|
||||
* @return array Column => SELECT query mapping.
|
||||
*/
|
||||
protected function get_segment_selections_product_level( $products_table ) {
|
||||
$columns_mapping = array(
|
||||
'items_sold' => "SUM($products_table.product_qty) as items_sold",
|
||||
'net_revenue' => "SUM($products_table.product_net_revenue ) AS net_revenue",
|
||||
'orders_count' => "COUNT( DISTINCT $products_table.order_id ) AS orders_count",
|
||||
'variations_count' => "COUNT( DISTINCT $products_table.variation_id ) AS variations_count",
|
||||
);
|
||||
|
||||
return $columns_mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
|
||||
*
|
||||
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
|
||||
* @param string $segmenting_from FROM part of segmenting SQL query.
|
||||
* @param string $segmenting_where WHERE part of segmenting SQL query.
|
||||
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
|
||||
* @param string $segmenting_dimension_name Name of the segmenting dimension.
|
||||
* @param string $table_name Name of SQL table which is the stats table for orders.
|
||||
* @param array $totals_query Array of SQL clauses for totals query.
|
||||
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) {
|
||||
global $wpdb;
|
||||
|
||||
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
|
||||
|
||||
// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
|
||||
// Product-level numbers.
|
||||
$segments_products = $wpdb->get_results(
|
||||
"SELECT
|
||||
$segmenting_groupby AS $segmenting_dimension_name
|
||||
{$segmenting_selections['product_level']}
|
||||
FROM
|
||||
$table_name
|
||||
$segmenting_from
|
||||
{$totals_query['from_clause']}
|
||||
WHERE
|
||||
1=1
|
||||
{$totals_query['where_time_clause']}
|
||||
{$totals_query['where_clause']}
|
||||
$segmenting_where
|
||||
GROUP BY
|
||||
$segmenting_groupby",
|
||||
ARRAY_A
|
||||
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
|
||||
|
||||
$totals_segments = $this->merge_segment_totals_results( $segmenting_dimension_name, $segments_products, array() );
|
||||
return $totals_segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
|
||||
*
|
||||
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
|
||||
* @param string $segmenting_from FROM part of segmenting SQL query.
|
||||
* @param string $segmenting_where WHERE part of segmenting SQL query.
|
||||
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
|
||||
* @param string $segmenting_dimension_name Name of the segmenting dimension.
|
||||
* @param string $table_name Name of SQL table which is the stats table for orders.
|
||||
* @param array $intervals_query Array of SQL clauses for intervals query.
|
||||
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) {
|
||||
global $wpdb;
|
||||
|
||||
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
|
||||
|
||||
// LIMIT offset, rowcount needs to be updated to a multiple of the number of segments.
|
||||
preg_match( '/LIMIT (\d+)\s?,\s?(\d+)/', $intervals_query['limit'], $limit_parts );
|
||||
$segment_count = count( $this->get_all_segments() );
|
||||
$orig_offset = intval( $limit_parts[1] );
|
||||
$orig_rowcount = intval( $limit_parts[2] );
|
||||
$segmenting_limit = $wpdb->prepare( 'LIMIT %d, %d', $orig_offset * $segment_count, $orig_rowcount * $segment_count );
|
||||
|
||||
// Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued).
|
||||
// Product-level numbers.
|
||||
$segments_products = $wpdb->get_results(
|
||||
"SELECT
|
||||
{$intervals_query['select_clause']} AS time_interval,
|
||||
$segmenting_groupby AS $segmenting_dimension_name
|
||||
{$segmenting_selections['product_level']}
|
||||
FROM
|
||||
$table_name
|
||||
$segmenting_from
|
||||
{$intervals_query['from_clause']}
|
||||
WHERE
|
||||
1=1
|
||||
{$intervals_query['where_time_clause']}
|
||||
{$intervals_query['where_clause']}
|
||||
$segmenting_where
|
||||
GROUP BY
|
||||
time_interval, $segmenting_groupby
|
||||
$segmenting_limit",
|
||||
ARRAY_A
|
||||
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
|
||||
|
||||
$intervals_segments = $this->merge_segment_intervals_results( $segmenting_dimension_name, $segments_products, array() );
|
||||
return $intervals_segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of segments formatted for REST response.
|
||||
*
|
||||
* @param string $type Type of segments to return--'totals' or 'intervals'.
|
||||
* @param array $query_params SQL query parameter array.
|
||||
* @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
|
||||
*
|
||||
* @return array
|
||||
* @throws \Automattic\WooCommerce\Admin\API\Reports\ParameterException In case of segmenting by variations, when no parent product is specified.
|
||||
*/
|
||||
protected function get_segments( $type, $query_params, $table_name ) {
|
||||
global $wpdb;
|
||||
if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
|
||||
$unique_orders_table = 'uniq_orders';
|
||||
$segmenting_where = '';
|
||||
|
||||
// Product, variation, and category are bound to product, so here product segmenting table is required,
|
||||
// while coupon and customer are bound to order, so we don't need the extra JOIN for those.
|
||||
// This also means that segment selections need to be calculated differently.
|
||||
if ( 'variation' === $this->query_args['segmentby'] ) {
|
||||
$product_level_columns = $this->get_segment_selections_product_level( $product_segmenting_table );
|
||||
$segmenting_selections = array(
|
||||
'product_level' => $this->prepare_selections( $product_level_columns ),
|
||||
);
|
||||
$this->report_columns = $product_level_columns;
|
||||
$segmenting_from = '';
|
||||
$segmenting_groupby = $product_segmenting_table . '.variation_id';
|
||||
$segmenting_dimension_name = 'variation_id';
|
||||
|
||||
// Restrict our search space for variation comparisons.
|
||||
if ( isset( $this->query_args['variation_includes'] ) ) {
|
||||
$variation_ids = implode( ',', $this->get_all_segments() );
|
||||
$segmenting_where = " AND $product_segmenting_table.variation_id IN ( $variation_ids )";
|
||||
}
|
||||
|
||||
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
|
||||
}
|
||||
|
||||
return $segments;
|
||||
}
|
||||
}
|
|
@ -145,6 +145,12 @@ class Analytics {
|
|||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/products',
|
||||
),
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-variations',
|
||||
'title' => __( 'Variations', 'woocommerce-admin' ),
|
||||
'parent' => 'woocommerce-analytics',
|
||||
'path' => '/analytics/variations',
|
||||
),
|
||||
array(
|
||||
'id' => 'woocommerce-analytics-categories',
|
||||
'title' => __( 'Categories', 'woocommerce-admin' ),
|
||||
|
|
|
@ -1048,6 +1048,8 @@ class Loader {
|
|||
// We may have synced orders with a now-unregistered status.
|
||||
// E.g An extension that added statuses is now inactive or removed.
|
||||
$settings['unregisteredOrderStatuses'] = self::get_unregistered_order_statuses();
|
||||
// The separator used for attributes found in Variation titles.
|
||||
$settings['variationTitleAttributesSeparator'] = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', new \WC_Product() );
|
||||
|
||||
if ( ! empty( $preload_data_endpoints ) ) {
|
||||
$settings['dataEndpoints'] = isset( $settings['dataEndpoints'] )
|
||||
|
|
|
@ -29,15 +29,16 @@ class ReportCSVEmail extends \WC_Email {
|
|||
$this->template_html = 'html-admin-report-export-download.php';
|
||||
$this->template_plain = 'plain-admin-report-export-download.php';
|
||||
$this->report_labels = array(
|
||||
'revenue' => __( 'Revenue', 'woocommerce-admin' ),
|
||||
'orders' => __( 'Orders', 'woocommerce-admin' ),
|
||||
'products' => __( 'Products', 'woocommerce-admin' ),
|
||||
'categories' => __( 'Categories', 'woocommerce-admin' ),
|
||||
'coupons' => __( 'Coupons', 'woocommerce-admin' ),
|
||||
'taxes' => __( 'Taxes', 'woocommerce-admin' ),
|
||||
'downloads' => __( 'Downloads', 'woocommerce-admin' ),
|
||||
'stock' => __( 'Stock', 'woocommerce-admin' ),
|
||||
'customers' => __( 'Customers', 'woocommerce-admin' ),
|
||||
'downloads' => __( 'Downloads', 'woocommerce-admin' ),
|
||||
'orders' => __( 'Orders', 'woocommerce-admin' ),
|
||||
'products' => __( 'Products', 'woocommerce-admin' ),
|
||||
'revenue' => __( 'Revenue', 'woocommerce-admin' ),
|
||||
'stock' => __( 'Stock', 'woocommerce-admin' ),
|
||||
'taxes' => __( 'Taxes', 'woocommerce-admin' ),
|
||||
'variations' => __( 'Variations', 'woocommerce-admin' ),
|
||||
);
|
||||
|
||||
// Call parent constructor.
|
||||
|
|
|
@ -117,7 +117,6 @@ class WC_Tests_API_Reports_Variations extends WC_REST_Unit_Test_Case {
|
|||
$request->set_query_params(
|
||||
array(
|
||||
'product_includes' => $variation->get_parent_id(),
|
||||
'products' => $variation->get_parent_id(),
|
||||
'variations' => $variation->get_id() . ',' . $variation_2->get_id(),
|
||||
)
|
||||
);
|
||||
|
|