Merge branch 'master' of github.com:woocommerce/wc-admin into fix/1352-backspace-remove-item

This commit is contained in:
Tiago Noronha 2019-02-21 15:50:21 +00:00
commit f28e74daf9
174 changed files with 8285 additions and 2545 deletions

View File

@ -26,6 +26,7 @@ wp-cli.local.yml
yarn.lock
tests
vendor
config
node_modules
*.sql
*.tar.gz

View File

@ -8,6 +8,7 @@ build-style
languages/*
!languages/README.md
wc-admin.zip
includes/feature-config.php
# Directories/files that may appear in your environment
.DS_Store

View File

@ -28,6 +28,10 @@ There are also some helper scripts:
- `npm run i18n` : A multi-step process, used to create a pot file from both the JS and PHP gettext calls. First it runs `i18n:js`, which creates a temporary `.pot` file from the JS files. Next it runs `i18n:php`, which converts that `.pot` file to a PHP file. Lastly, it runs `i18n:pot`, which creates the final `.pot` file from all the PHP files in the plugin (including the generated one with the JS strings).
- `npm test` : Run the JS test suite
To debug synced lookup information in the database, you can bypass the action scheduler and immediately sync order and customer information by using the `woocommerce_disable_order_scheduling` hook.
`add_filter( 'woocommerce_disable_order_scheduling', '__return_true' );`
## Privacy
If you have enabled WooCommerce usage tracking ( option `woocommerce_allow_tracking` ) then, in addition to the tracking described in https://woocommerce.com/usage-tracking/, this plugin also sends information about the actions that site administrators perform to Automattic - see https://automattic.com/privacy/#information-we-collect-automatically for more information.

View File

@ -63,6 +63,7 @@ fi
# Run the build.
status "Generating build... 👷‍♀️"
npm run build:feature-config
npm run build
npm run docs
@ -79,6 +80,6 @@ zip -r wc-admin.zip \
$build_files \
languages/wc-admin.pot \
languages/wc-admin.php \
README.md
readme.txt
success "Done. You've built WooCommerce Admin! 🎉 "

View File

@ -0,0 +1,28 @@
<?php
/**
* Generates an array of feature flags, based on the config used by the client application.
*
* @package WooCommerce Admin
*/
$phase = isset( $_SERVER['WC_ADMIN_PHASE'] ) ? $_SERVER['WC_ADMIN_PHASE'] : ''; // WPCS: sanitization ok.
if ( ! in_array( $phase, array( 'development', 'plugin', 'core' ) ) ) {
$phase = 'core';
}
$config_json = file_get_contents( 'config/' . $phase . '.json' );
$config = json_decode( $config_json );
$write = "<?php\n";
$write .= "// WARNING: Do not directly edit this file.\n";
$write .= "// This file is auto-generated as part of the build process and things may break.\n";
$write .= "function wc_admin_get_feature_config() {\n";
$write .= "\treturn array(\n";
foreach ( $config->features as $feature => $bool ) {
$write .= "\t\t'{$feature}' => " . ( $bool ? 'true' : 'false' ) . ",\n";
}
$write .= "\t);\n";
$write .= "}\n";
$config_file = fopen( 'includes/feature-config.php', 'w' );
fwrite( $config_file, $write );
fclose( $config_file );

View File

@ -2,6 +2,7 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { format as formatDate } from '@wordpress/date';
@ -89,6 +90,7 @@ export class ReportChart extends Component {
renderChart( mode, isRequesting, chartData ) {
const {
emptySearchResults,
interactiveLegend,
itemsLabel,
legendPosition,
@ -101,11 +103,15 @@ export class ReportChart extends Component {
const currentInterval = getIntervalForQuery( query );
const allowedIntervals = getAllowedIntervalsForQuery( query );
const formats = getDateFormatsForInterval( currentInterval, primaryData.data.intervals.length );
const emptyMessage = emptySearchResults
? __( 'No data for the current search', 'wc-admin' )
: __( 'No data for the selected date range', 'wc-admin' );
return (
<Chart
allowedIntervals={ allowedIntervals }
data={ chartData }
dateParser={ '%Y-%m-%dT%H:%M:%S' }
emptyMessage={ emptyMessage }
interactiveLegend={ interactiveLegend }
interval={ currentInterval }
isRequesting={ isRequesting }
@ -119,7 +125,7 @@ export class ReportChart extends Component {
tooltipLabelFormat={ formats.tooltipLabelFormat }
tooltipTitle={ ( 'time-comparison' === mode && selectedChart.label ) || null }
tooltipValueFormat={ getTooltipValueFormat( selectedChart.type ) }
type={ getChartTypeForQuery( query ) }
chartType={ getChartTypeForQuery( query ) }
valueType={ selectedChart.type }
xFormat={ formats.xFormat }
x2Format={ formats.x2Format }
@ -222,6 +228,7 @@ export default compose(
if ( query.search && ! ( query[ endpoint ] && query[ endpoint ].length ) ) {
return {
emptySearchResults: true,
mode: chartMode,
};
}

View File

@ -61,8 +61,15 @@ export class ReportSummary extends Component {
const renderSummaryNumbers = ( { onToggle } ) =>
charts.map( chart => {
const { key, label, type } = chart;
const href = getNewPath( { chart: key } );
const { key, order, orderby, label, type } = chart;
const newPath = { chart: key };
if ( orderby ) {
newPath.orderby = orderby;
}
if ( order ) {
newPath.order = order;
}
const href = getNewPath( newPath );
const isSelected = selectedChart.key === key;
const { delta, prevValue, value } = this.getValues( key, type );

View File

@ -194,7 +194,9 @@ export default compose(
if ( query.search && ! ( query[ endpoint ] && query[ endpoint ].length ) ) {
return {};
}
const chartEndpoint = 'variations' === endpoint ? 'products' : endpoint;
const chartEndpoint = [ 'variations', 'categories' ].includes( endpoint )
? 'products'
: endpoint;
const primaryData = getSummary
? getReportChartData( chartEndpoint, 'primary', query, select )
: {};

View File

@ -13,16 +13,22 @@ export const charts = [
{
key: 'items_sold',
label: __( 'Items Sold', 'wc-admin' ),
order: 'desc',
orderby: 'items_sold',
type: 'number',
},
{
key: 'net_revenue',
label: __( 'Net Revenue', 'wc-admin' ),
order: 'desc',
orderby: 'net_revenue',
type: 'currency',
},
{
key: 'orders_count',
label: __( 'Orders Count', 'wc-admin' ),
order: 'desc',
orderby: 'orders_count',
type: 'number',
},
];
@ -30,7 +36,7 @@ export const charts = [
export const filters = [
{
label: __( 'Show', 'wc-admin' ),
staticParams: [ 'chart' ],
staticParams: [],
param: 'filter',
showFilters: () => true,
filters: [
@ -77,13 +83,13 @@ export const filters = [
label: __( 'Top Categories by Items Sold', 'wc-admin' ),
value: 'top_items',
chartMode: 'item-comparison',
query: { orderby: 'items_sold', order: 'desc' },
query: { orderby: 'items_sold', order: 'desc', chart: 'items_sold' },
},
{
label: __( 'Top Categories by Net Revenue', 'wc-admin' ),
value: 'top_revenue',
chartMode: 'item-comparison',
query: { orderby: 'net_revenue', order: 'desc' },
query: { orderby: 'net_revenue', order: 'desc', chart: 'net_revenue' },
},
],
},

View File

@ -4,6 +4,7 @@
*/
import { Component, Fragment } from '@wordpress/element';
import PropTypes from 'prop-types';
import { __ } from '@wordpress/i18n';
/**
* WooCommerce dependencies
@ -20,23 +21,50 @@ import ReportChart from 'analytics/components/report-chart';
import ReportSummary from 'analytics/components/report-summary';
export default class CategoriesReport extends Component {
getChartMeta() {
const { query } = this.props;
const isCategoryDetailsView = [ 'top_items', 'top_revenue', 'compare-categories' ].includes(
query.filter
);
const mode = isCategoryDetailsView ? 'item-comparison' : 'time-comparison';
const itemsLabel = __( '%d categories', 'wc-admin' );
return {
itemsLabel,
mode,
};
}
render() {
const { query, path } = this.props;
const { mode, itemsLabel } = this.getChartMeta();
const chartQuery = {
...query,
};
if ( 'item-comparison' === mode ) {
chartQuery.segmentby = 'category';
}
return (
<Fragment>
<ReportFilters query={ query } path={ path } filters={ filters } />
<ReportSummary
charts={ charts }
endpoint="categories"
query={ query }
endpoint="products"
query={ chartQuery }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
<ReportChart
filters={ filters }
charts={ charts }
endpoint="categories"
mode={ mode }
endpoint="products"
path={ path }
query={ query }
query={ chartQuery }
itemsLabel={ itemsLabel }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
<CategoriesReportTable query={ query } />
@ -47,4 +75,5 @@ export default class CategoriesReport extends Component {
CategoriesReport.propTypes = {
query: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
};

View File

@ -13,11 +13,15 @@ export const charts = [
{
key: 'orders_count',
label: __( 'Discounted Orders', 'wc-admin' ),
order: 'desc',
orderby: 'orders_count',
type: 'number',
},
{
key: 'amount',
label: __( 'Amount', 'wc-admin' ),
order: 'desc',
orderby: 'amount',
type: 'currency',
},
];
@ -66,8 +70,18 @@ export const filters = [
},
},
},
{ label: __( 'Top Coupons by Discounted Orders', 'wc-admin' ), value: 'top_orders' },
{ label: __( 'Top Coupons by Amount Discounted', 'wc-admin' ), value: 'top_discount' },
{
label: __( 'Top Coupons by Discounted Orders', 'wc-admin' ),
value: 'top_orders',
chartMode: 'item-comparison',
query: { orderby: 'orders_count', order: 'desc', chart: 'orders_count' },
},
{
label: __( 'Top Coupons by Amount Discounted', 'wc-admin' ),
value: 'top_discount',
chartMode: 'item-comparison',
query: { orderby: 'amount', order: 'desc', chart: 'amount' },
},
],
},
];

View File

@ -4,6 +4,7 @@
*/
import { Component, Fragment } from '@wordpress/element';
import PropTypes from 'prop-types';
import { __ } from '@wordpress/i18n';
/**
* WooCommerce dependencies
@ -20,8 +21,32 @@ import ReportChart from 'analytics/components/report-chart';
import ReportSummary from 'analytics/components/report-summary';
export default class CouponsReport extends Component {
getChartMeta() {
const { query } = this.props;
const isCompareView = [ 'top_orders', 'top_discount', 'compare-coupons' ].includes(
query.filter
);
const mode = isCompareView ? 'item-comparison' : 'time-comparison';
const itemsLabel = __( '%d coupons', 'wc-admin' );
return {
itemsLabel,
mode,
};
}
render() {
const { query, path } = this.props;
const { mode, itemsLabel } = this.getChartMeta();
const chartQuery = {
...query,
};
if ( 'item-comparison' === mode ) {
chartQuery.segmentby = 'coupon';
}
return (
<Fragment>
@ -29,14 +54,17 @@ export default class CouponsReport extends Component {
<ReportSummary
charts={ charts }
endpoint="coupons"
query={ query }
query={ chartQuery }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
<ReportChart
filters={ filters }
charts={ charts }
mode={ mode }
endpoint="coupons"
path={ path }
query={ query }
query={ chartQuery }
itemsLabel={ itemsLabel }
selectedChart={ getSelectedChart( query.chart, charts ) }
/>
<CouponsReportTable query={ query } />

View File

@ -39,7 +39,7 @@ export const advancedFilters = {
remove: __( 'Remove customer name filter', 'wc-admin' ),
rule: __( 'Select a customer name filter match', 'wc-admin' ),
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
title: __( 'Name {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Name{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select customer name', 'wc-admin' ),
},
rules: [
@ -59,7 +59,7 @@ export const advancedFilters = {
type: 'customers',
getLabels: getRequestByIdString( NAMESPACE + '/customers', customer => ( {
id: customer.id,
label: [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' ),
label: customer.name,
} ) ),
},
},
@ -70,7 +70,7 @@ export const advancedFilters = {
remove: __( 'Remove country filter', 'wc-admin' ),
rule: __( 'Select a country filter match', 'wc-admin' ),
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
title: __( 'Country {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Country{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select country', 'wc-admin' ),
},
rules: [
@ -111,7 +111,7 @@ export const advancedFilters = {
remove: __( 'Remove customer username filter', 'wc-admin' ),
rule: __( 'Select a customer username filter match', 'wc-admin' ),
/* translators: A sentence describing a customer username filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
title: __( 'Username {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Username{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select customer username', 'wc-admin' ),
},
rules: [
@ -139,7 +139,7 @@ export const advancedFilters = {
remove: __( 'Remove customer email filter', 'wc-admin' ),
rule: __( 'Select a customer email filter match', 'wc-admin' ),
/* translators: A sentence describing a customer email filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
title: __( 'Email {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Email{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select customer email', 'wc-admin' ),
},
rules: [
@ -168,7 +168,7 @@ export const advancedFilters = {
add: __( 'No. of Orders', 'wc-admin' ),
remove: __( 'Remove order filter', 'wc-admin' ),
rule: __( 'Select an order count filter match', 'wc-admin' ),
title: __( 'No. of Orders {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}No. of Orders{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
},
rules: [
{
@ -196,7 +196,7 @@ export const advancedFilters = {
add: __( 'Total Spend', 'wc-admin' ),
remove: __( 'Remove total spend filter', 'wc-admin' ),
rule: __( 'Select a total spend filter match', 'wc-admin' ),
title: __( 'Total Spend {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Total Spend{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
},
rules: [
{
@ -224,7 +224,7 @@ export const advancedFilters = {
add: __( 'AOV', 'wc-admin' ),
remove: __( 'Remove average older value filter', 'wc-admin' ),
rule: __( 'Select an average order value filter match', 'wc-admin' ),
title: __( 'AOV {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}AOV{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
},
rules: [
{
@ -254,7 +254,7 @@ export const advancedFilters = {
remove: __( 'Remove registered filter', 'wc-admin' ),
rule: __( 'Select a registered filter match', 'wc-admin' ),
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
title: __( 'Registered {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Registered{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select registered date', 'wc-admin' ),
},
rules: [
@ -284,7 +284,7 @@ export const advancedFilters = {
remove: __( 'Remove last active filter', 'wc-admin' ),
rule: __( 'Select a last active filter match', 'wc-admin' ),
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cCsm3GeXJbE */
title: __( 'Last active {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Last active{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select registered date', 'wc-admin' ),
},
rules: [

View File

@ -45,7 +45,7 @@ export const advancedFilters = {
remove: __( 'Remove product filter', 'wc-admin' ),
rule: __( 'Select a product filter match', 'wc-admin' ),
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
title: __( 'Product {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Product{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select product', 'wc-admin' ),
},
rules: [
@ -66,14 +66,14 @@ export const advancedFilters = {
getLabels: getProductLabels,
},
},
user: {
customer: {
labels: {
add: __( 'Username', 'wc-admin' ),
placeholder: __( 'Search customer username', 'wc-admin' ),
remove: __( 'Remove customer username filter', 'wc-admin' ),
rule: __( 'Select a customer username filter match', 'wc-admin' ),
/* translators: A sentence describing a customer username filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
title: __( 'Username {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Username{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select customer username', 'wc-admin' ),
},
rules: [
@ -101,7 +101,7 @@ export const advancedFilters = {
remove: __( 'Remove order number filter', 'wc-admin' ),
rule: __( 'Select a order number filter match', 'wc-admin' ),
/* translators: A sentence describing a order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
title: __( 'Order number {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Order number{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select order number', 'wc-admin' ),
},
rules: [
@ -135,7 +135,7 @@ export const advancedFilters = {
remove: __( 'Remove IP address filter', 'wc-admin' ),
rule: __( 'Select an IP address filter match', 'wc-admin' ),
/* translators: A sentence describing a order number filter. See screen shot for context: https://cloudup.com/ccxhyH2mEDg */
title: __( 'IP Address {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}IP Address{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select IP address', 'wc-admin' ),
},
rules: [

View File

@ -70,9 +70,17 @@ export default class CouponsReportTable extends Component {
const persistedQuery = getPersistedQuery( query );
return map( downloads, download => {
const { _embedded, date, file_name, file_path, ip_address, order_id, product_id } = download;
const {
_embedded,
date,
file_name,
file_path,
ip_address,
order_id,
product_id,
username,
} = download;
const { name: productName } = _embedded.product[ 0 ];
const { name: userName } = _embedded.user[ 0 ];
const productLink = getNewPath( persistedQuery, 'products', {
filter: 'single_product',
@ -109,8 +117,8 @@ export default class CouponsReportTable extends Component {
value: order_id,
},
{
display: userName,
value: userName,
display: username,
value: username,
},
{
display: ip_address,

View File

@ -35,7 +35,7 @@ import withSelect from 'wc-api/with-select';
const REPORTS_FILTER = 'woocommerce-reports-list';
const getReports = () => {
const reports = applyFilters( REPORTS_FILTER, [
const reports = [
{
report: 'revenue',
title: __( 'Revenue', 'wc-admin' ),
@ -86,9 +86,9 @@ const getReports = () => {
title: __( 'Downloads', 'wc-admin' ),
component: DownloadsReport,
},
] );
];
return reports;
return applyFilters( REPORTS_FILTER, reports );
};
class Report extends Component {
@ -145,15 +145,22 @@ export default compose(
}
const { report } = props.params;
const items = searchItemsByString( select, report, search );
const searchWords = search.split( ',' ).map( searchWord => searchWord.replace( '%2C', ',' ) );
const items = searchItemsByString( select, report, searchWords );
const ids = Object.keys( items );
if ( ! ids.length ) {
return {};
return {
query: {
...props.query,
search: searchWords,
},
};
}
return {
query: {
...props.query,
search: searchWords,
[ report ]: ids.join( ',' ),
},
};

View File

@ -20,6 +20,8 @@ export const charts = [
{
key: 'net_revenue',
label: __( 'Net Revenue', 'wc-admin' ),
order: 'desc',
orderby: 'net_total',
type: 'currency',
},
{
@ -30,6 +32,8 @@ export const charts = [
{
key: 'avg_items_per_order',
label: __( 'Average Items Per Order', 'wc-admin' ),
order: 'desc',
orderby: 'num_items_sold',
type: 'average',
},
];
@ -61,7 +65,7 @@ export const advancedFilters = {
remove: __( 'Remove order status filter', 'wc-admin' ),
rule: __( 'Select an order status filter match', 'wc-admin' ),
/* translators: A sentence describing an Order Status filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
title: __( 'Order Status {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Order Status{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select an order status', 'wc-admin' ),
},
rules: [
@ -91,7 +95,7 @@ export const advancedFilters = {
remove: __( 'Remove products filter', 'wc-admin' ),
rule: __( 'Select a product filter match', 'wc-admin' ),
/* translators: A sentence describing a Product filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
title: __( 'Product {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Product{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select products', 'wc-admin' ),
},
rules: [
@ -119,7 +123,7 @@ export const advancedFilters = {
remove: __( 'Remove coupon filter', 'wc-admin' ),
rule: __( 'Select a coupon filter match', 'wc-admin' ),
/* translators: A sentence describing a Coupon filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
title: __( 'Coupon Code {{rule /}} {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Coupon Code{{/title}} {{rule /}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select coupon codes', 'wc-admin' ),
},
rules: [
@ -146,7 +150,7 @@ export const advancedFilters = {
remove: __( 'Remove customer filter', 'wc-admin' ),
rule: __( 'Select a customer filter match', 'wc-admin' ),
/* translators: A sentence describing a Customer filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
title: __( 'Customer is {{filter /}}', 'wc-admin' ),
title: __( '{{title}}Customer is{{/title}} {{filter /}}', 'wc-admin' ),
filter: __( 'Select a customer type', 'wc-admin' ),
},
input: {

View File

@ -45,7 +45,6 @@ export default class OrdersReportTable extends Component {
screenReaderLabel: __( 'Order ID', 'wc-admin' ),
key: 'id',
required: true,
isSortable: true,
},
{
label: __( 'Status', 'wc-admin' ),
@ -68,9 +67,9 @@ export default class OrdersReportTable extends Component {
},
{
label: __( 'Items Sold', 'wc-admin' ),
key: 'items_sold',
key: 'num_items_sold',
required: false,
isSortable: false,
isSortable: true,
isNumeric: true,
},
{
@ -83,9 +82,9 @@ export default class OrdersReportTable extends Component {
{
label: __( 'N. Revenue', 'wc-admin' ),
screenReaderLabel: __( 'Net Revenue', 'wc-admin' ),
key: 'net_revenue',
key: 'net_total',
required: true,
isSortable: false,
isSortable: true,
isNumeric: true,
},
];

View File

@ -13,16 +13,22 @@ export const charts = [
{
key: 'items_sold',
label: __( 'Items Sold', 'wc-admin' ),
order: 'desc',
orderby: 'items_sold',
type: 'number',
},
{
key: 'net_revenue',
label: __( 'Net Revenue', 'wc-admin' ),
order: 'desc',
orderby: 'net_revenue',
type: 'currency',
},
{
key: 'orders_count',
label: __( 'Orders Count', 'wc-admin' ),
order: 'desc',
orderby: 'orders_count',
type: 'number',
},
];

View File

@ -28,10 +28,13 @@ class ProductsReport extends Component {
getChartMeta() {
const { query, isSingleProductView, isSingleProductVariable } = this.props;
const isProductDetailsView =
'top_items' === query.filter ||
'top_sales' === query.filter ||
'compare-products' === query.filter;
const isProductDetailsView = [
'top_items',
'top_sales',
'compare-products',
'single_category',
'compare-categories',
].includes( query.filter );
const mode =
isProductDetailsView || ( isSingleProductView && isSingleProductVariable )
@ -41,8 +44,8 @@ class ProductsReport extends Component {
isSingleProductView && isSingleProductVariable ? 'variations' : 'products';
const label =
isSingleProductView && isSingleProductVariable
? __( '%s variations', 'wc-admin' )
: __( '%s products', 'wc-admin' );
? __( '%d variations', 'wc-admin' )
: __( '%d products', 'wc-admin' );
return {
compareObject,
@ -130,7 +133,8 @@ export default compose(
withSelect( ( select, props ) => {
const { query } = props;
const { getItems, isGetItemsRequesting, getItemsError } = select( 'wc-api' );
const isSingleProductView = query.products && 1 === query.products.split( ',' ).length;
const isSingleProductView =
! query.search && query.products && 1 === query.products.split( ',' ).length;
if ( isSingleProductView ) {
const productId = parseInt( query.products );
const includeArgs = { include: productId };

View File

@ -84,7 +84,7 @@ export default class VariationsReportTable extends Component {
return map( data, row => {
const { items_sold, net_revenue, orders_count, extended_info, product_id } = row;
const { stock_status, stock_quantity, low_stock_amount, sku } = extended_info;
const name = get( row, [ 'extended_info', 'name' ], '' ).replace( ' - ', ' / ' );
const name = get( row, [ 'extended_info', 'name' ], '' );
const ordersLink = getNewPath( persistedQuery, 'orders', {
filter: 'advanced',
product_includes: query.products,

View File

@ -8,31 +8,41 @@ export const charts = [
{
key: 'gross_revenue',
label: __( 'Gross Revenue', 'wc-admin' ),
order: 'desc',
orderby: 'gross_revenue',
type: 'currency',
},
{
key: 'refunds',
label: __( 'Refunds', 'wc-admin' ),
order: 'desc',
orderby: 'refunds',
type: 'currency',
},
{
key: 'coupons',
label: __( 'Coupons', 'wc-admin' ),
order: 'desc',
orderby: 'coupons',
type: 'currency',
},
{
key: 'taxes',
label: __( 'Taxes', 'wc-admin' ),
order: 'desc',
orderby: 'taxes',
type: 'currency',
},
{
key: 'shipping',
label: __( 'Shipping', 'wc-admin' ),
orderby: 'shipping',
type: 'currency',
},
{
key: 'net_revenue',
label: __( 'Net Revenue', 'wc-admin' ),
orderby: 'net_revenue',
type: 'currency',
},
];

View File

@ -240,8 +240,8 @@ export default compose(
return {
tableData: {
items: {
data: get( revenueData, [ 'data', 'intervals' ] ),
totalResults: get( revenueData, [ 'totalResults' ] ),
data: get( revenueData, [ 'data', 'intervals' ], [] ),
totalResults: get( revenueData, [ 'totalResults' ], 0 ),
},
isError,
isRequesting,

View File

@ -17,6 +17,7 @@ export const filters = [
{ label: __( 'Out of Stock', 'wc-admin' ), value: 'outofstock' },
{ label: __( 'Low Stock', 'wc-admin' ), value: 'lowstock' },
{ label: __( 'In Stock', 'wc-admin' ), value: 'instock' },
{ label: __( 'On Backorder', 'wc-admin' ), value: 'onbackorder' },
],
},
];

View File

@ -67,16 +67,14 @@ export default class StockReportTable extends Component {
products: parent_id || id,
} );
const formattedName = name.replace( ' - ', ' / ' );
const nameLink = (
<Link href={ productDetailLink } type="wc-admin">
{ formattedName }
{ name }
</Link>
);
const stockStatusLink = (
<Link href={ 'post.php?action=edit&post=' + parent_id || id } type="wp-admin">
<Link href={ 'post.php?action=edit&post=' + ( parent_id || id ) } type="wp-admin">
{ stockStatuses[ stock_status ] }
</Link>
);
@ -84,7 +82,7 @@ export default class StockReportTable extends Component {
return [
{
display: nameLink,
value: formattedName,
value: name,
},
{
display: sku,
@ -103,23 +101,27 @@ export default class StockReportTable extends Component {
}
getSummary( totals ) {
const { products = 0, out_of_stock = 0, low_stock = 0, in_stock = 0 } = totals;
const { products = 0, outofstock = 0, lowstock = 0, instock = 0, onbackorder = 0 } = totals;
return [
{
label: _n( 'product', 'products', products, 'wc-admin' ),
value: numberFormat( products ),
},
{
label: __( 'out of stock', out_of_stock, 'wc-admin' ),
value: numberFormat( out_of_stock ),
label: __( 'out of stock', outofstock, 'wc-admin' ),
value: numberFormat( outofstock ),
},
{
label: __( 'low stock', low_stock, 'wc-admin' ),
value: numberFormat( low_stock ),
label: __( 'low stock', lowstock, 'wc-admin' ),
value: numberFormat( lowstock ),
},
{
label: __( 'in stock', in_stock, 'wc-admin' ),
value: numberFormat( in_stock ),
label: __( 'on backorder', onbackorder, 'wc-admin' ),
value: numberFormat( onbackorder ),
},
{
label: __( 'in stock', instock, 'wc-admin' ),
value: numberFormat( instock ),
},
];
}
@ -132,7 +134,7 @@ export default class StockReportTable extends Component {
endpoint="stock"
getHeadersContent={ this.getHeadersContent }
getRowsContent={ this.getRowsContent }
// getSummary={ this.getSummary }
getSummary={ this.getSummary }
query={ query }
tableQuery={ {
orderby: query.orderby || 'stock_status',

View File

@ -15,21 +15,29 @@ export const charts = [
{
key: 'total_tax',
label: __( 'Total Tax', 'wc-admin' ),
order: 'desc',
orderby: 'total_tax',
type: 'currency',
},
{
key: 'order_tax',
label: __( 'Order Tax', 'wc-admin' ),
order: 'desc',
orderby: 'order_tax',
type: 'currency',
},
{
key: 'shipping_tax',
label: __( 'Shipping Tax', 'wc-admin' ),
order: 'desc',
orderby: 'shipping_tax',
type: 'currency',
},
{
key: 'orders_count',
label: __( 'Orders Count', 'wc-admin' ),
order: 'desc',
orderby: 'orders_count',
type: 'number',
},
];

View File

@ -21,6 +21,7 @@ import './index.scss';
import { analyticsSettings } from './config';
import Header from 'header';
import Setting from './setting';
import withSelect from 'wc-api/with-select';
const SETTINGS_FILTER = 'woocommerce_admin_analytics_settings';
@ -33,6 +34,7 @@ class Settings extends Component {
this.state = {
settings: settings,
saving: false,
};
this.handleInputChange = this.handleInputChange.bind( this );
@ -59,9 +61,31 @@ class Settings extends Component {
}
};
componentDidUpdate() {
const { addNotice, isError, isRequesting } = this.props;
const { saving } = this.state;
if ( saving && ! isRequesting ) {
if ( ! isError ) {
addNotice( {
status: 'success',
message: __( 'Your settings have been successfully saved.', 'wc-admin' ),
} );
} else {
addNotice( {
status: 'error',
message: __( 'There was an error saving your settings. Please try again.', 'wc-admin' ),
} );
}
/* eslint-disable react/no-did-update-set-state */
this.setState( { saving: false } );
/* eslint-enable react/no-did-update-set-state */
}
}
saveChanges = () => {
this.props.updateSettings( this.state.settings );
// @todo Need a confirmation on successful update.
this.setState( { saving: true } );
};
handleInputChange( e ) {
@ -124,10 +148,21 @@ class Settings extends Component {
}
export default compose(
withSelect( select => {
const { getSettings, getSettingsError, isGetSettingsRequesting } = select( 'wc-api' );
const settings = getSettings();
const isError = Boolean( getSettingsError() );
const isRequesting = isGetSettingsRequesting();
return { getSettings, isError, isRequesting, settings };
} ),
withDispatch( dispatch => {
const { addNotice } = dispatch( 'wc-admin' );
const { updateSettings } = dispatch( 'wc-api' );
return {
addNotice,
updateSettings,
};
} )

View File

@ -78,11 +78,11 @@ class DashboardCharts extends Component {
};
}
handleTypeToggle( type ) {
handleTypeToggle( chartType ) {
return () => {
this.setState( { chartType: type } );
this.setState( { chartType } );
const userDataFields = {
[ 'dashboard_chart_type' ]: type,
[ 'dashboard_chart_type' ]: chartType,
};
this.props.updateCurrentUserData( userDataFields );
};
@ -146,7 +146,7 @@ class DashboardCharts extends Component {
render() {
const { path } = this.props;
const { chartType, hiddenChartKeys, interval } = this.state;
const query = { ...this.props.query, type: chartType, interval };
const query = { ...this.props.query, chartType, interval };
return (
<Fragment>
<div className="woocommerce-dashboard__dashboard-charts">
@ -163,24 +163,25 @@ class DashboardCharts extends Component {
>
<IconButton
className={ classNames( 'woocommerce-chart__type-button', {
'woocommerce-chart__type-button-selected': ! query.type || query.type === 'line',
'woocommerce-chart__type-button-selected':
! query.chartType || query.chartType === 'line',
} ) }
icon={ <Gridicon icon="line-graph" /> }
title={ __( 'Line chart', 'wc-admin' ) }
aria-checked={ query.type === 'line' }
aria-checked={ query.chartType === 'line' }
role="menuitemradio"
tabIndex={ query.type === 'line' ? 0 : -1 }
tabIndex={ query.chartType === 'line' ? 0 : -1 }
onClick={ this.handleTypeToggle( 'line' ) }
/>
<IconButton
className={ classNames( 'woocommerce-chart__type-button', {
'woocommerce-chart__type-button-selected': query.type === 'bar',
'woocommerce-chart__type-button-selected': query.chartType === 'bar',
} ) }
icon={ <Gridicon icon="stats-alt" /> }
title={ __( 'Bar chart', 'wc-admin' ) }
aria-checked={ query.type === 'bar' }
aria-checked={ query.chartType === 'bar' }
role="menuitemradio"
tabIndex={ query.type === 'bar' ? 0 : -1 }
tabIndex={ query.chartType === 'bar' ? 0 : -1 }
onClick={ this.handleTypeToggle( 'bar' ) }
/>
</NavigableMenu>

View File

@ -22,7 +22,6 @@ import { formatCurrency } from '@woocommerce/currency';
* Internal dependencies
*/
import {
Card,
EllipsisMenu,
MenuItem,
MenuTitle,
@ -160,7 +159,7 @@ class StorePerformance extends Component {
return (
<Fragment>
<SectionHeader title={ __( 'Store Performance', 'wc-admin' ) } menu={ this.renderMenu() } />
<Card className="woocommerce-dashboard__store-performance">{ this.renderList() }</Card>
<div className="woocommerce-dashboard__store-performance">{ this.renderList() }</div>
</Fragment>
);
}

View File

@ -1,18 +1,40 @@
/** @format */
.woocommerce-dashboard__store-performance {
border-bottom: 0;
border-right: 0;
margin-bottom: $gap-large;
.woocommerce-card__header {
border-right: 1px solid $core-grey-light-700;
}
.woocommerce-card__body {
padding: 0;
@include breakpoint( '<782px' ) {
border-width: 0;
}
.woocommerce-summary {
background-color: $core-grey-light-100;
margin: 0;
@include breakpoint( '<782px' ) {
&.is-placeholder {
border-top: 0;
}
&:not(.is-placeholder) {
.woocommerce-summary__item-container:first-child {
.woocommerce-summary__item {
border-top: 1px solid $core-grey-light-700;
}
}
}
}
}
.woocommerce-summary__item {
background-color: $white;
&:hover {
background-color: $core-grey-light-200;
}
&:active {
background-color: $core-grey-light-300;
}
}
}

View File

@ -18,6 +18,7 @@
{ "component": "ProductImage" },
{ "component": "Rating" },
{ "component": "Search" },
{ "component": "SearchListControl" },
{ "component": "Section" },
{ "component": "SegmentedSelection" },
{ "component": "SplitButton" },

View File

@ -10,6 +10,7 @@ import { Provider as SlotFillProvider } from 'react-slot-fill';
*/
import './stylesheets/_embedded.scss';
import { EmbedLayout } from './layout';
import 'store';
import 'wc-api/wp-data-store';
render(

View File

@ -24,7 +24,7 @@ import {
Section,
} from '@woocommerce/components';
import { formatCurrency, getCurrencyFormatDecimal } from '@woocommerce/currency';
import { getAdminLink } from '@woocommerce/navigation';
import { getAdminLink, getNewPath } from '@woocommerce/navigation';
/**
* Internal dependencies
@ -32,7 +32,6 @@ import { getAdminLink } from '@woocommerce/navigation';
import { ActivityCard, ActivityCardPlaceholder } from '../activity-card';
import ActivityHeader from '../activity-header';
import ActivityOutboundLink from '../activity-outbound-link';
import { getOrderRefundTotal } from 'lib/order-values';
import { QUERY_DEFAULTS } from 'wc-api/constants';
import withSelect from 'wc-api/with-select';
@ -64,28 +63,56 @@ function OrdersPanel( { orders, isRequesting, isError } ) {
</EllipsisMenu>
);
const orderCardTitle = ( order, address ) => {
const name = `${ address.first_name } ${ address.last_name }`;
const getCustomerString = order => {
const extended_info = order.extended_info || {};
const { first_name, last_name } = extended_info.customer || {};
if ( ! first_name && ! last_name ) {
return '';
}
const name = [ first_name, last_name ].join( ' ' );
return sprintf(
__(
/* translators: describes who placed an order, e.g. Order #123 placed by John Doe */
'placed by {{customerLink}}%(customerName)s{{/customerLink}}',
'wc-admin'
),
{
customerName: name,
}
);
};
const orderCardTitle = order => {
const { extended_info, order_id } = order;
const { customer } = extended_info || {};
const customerUrl = customer.customer_id
? getNewPath( {}, '/analytics/customers', {
filter: 'single_customer',
customer_id: customer.customer_id,
} )
: null;
return (
<Fragment>
{ interpolateComponents( {
mixedString: sprintf(
__(
/* eslint-disable-next-line max-len */
'Order {{orderLink}}#%(orderNumber)s{{/orderLink}} placed by {{customerLink}}%(customerName)s{{/customerLink}} {{destinationFlag/}}',
'Order {{orderLink}}#%(orderNumber)s{{/orderLink}} %(customerString)s {{destinationFlag/}}',
'wc-admin'
),
{
orderNumber: order.number,
customerName: name,
orderNumber: order_id,
customerString: getCustomerString( order ),
}
),
components: {
orderLink: <Link href={ 'post.php?action=edit&post=' + order.id } type="wp-admin" />,
// @todo Hook up customer name link
customerLink: <Link href={ '#' } type="wp-admin" />,
destinationFlag: <Flag order={ order } round={ false } />,
orderLink: <Link href={ 'post.php?action=edit&post=' + order_id } type="wp-admin" />,
destinationFlag: customer.country ? (
<Flag code={ customer.country } round={ false } />
) : null,
customerLink: customerUrl ? <Link href={ customerUrl } type="wc-admin" /> : <span />,
},
} ) }
</Fragment>
@ -93,20 +120,20 @@ function OrdersPanel( { orders, isRequesting, isError } ) {
};
const cards = [];
orders.forEach( ( order, id ) => {
// We want the billing address, but shipping can be used as a fallback.
const address = { ...order.shipping, ...order.billing };
const productsCount = order.line_items.reduce( ( total, line ) => total + line.quantity, 0 );
orders.forEach( order => {
const extended_info = order.extended_info || {};
const productsCount =
extended_info && extended_info.products ? extended_info.products.length : 0;
const total = order.total;
const refundValue = getOrderRefundTotal( order );
const remainingTotal = getCurrencyFormatDecimal( order.total ) + refundValue;
const total = order.gross_total;
const refundValue = order.refund_total;
const remainingTotal = getCurrencyFormatDecimal( total ) + refundValue;
cards.push(
<ActivityCard
key={ id }
key={ order.order_id }
className="woocommerce-order-activity-card"
title={ orderCardTitle( order, address ) }
title={ orderCardTitle( order ) }
date={ order.date_created }
subtitle={
<div>
@ -118,16 +145,15 @@ function OrdersPanel( { orders, isRequesting, isError } ) {
</span>
{ refundValue ? (
<span>
<s>{ formatCurrency( total, order.currency_symbol ) }</s>{' '}
{ formatCurrency( remainingTotal, order.currency_symbol ) }
<s>{ formatCurrency( total ) }</s> { formatCurrency( remainingTotal ) }
</span>
) : (
<span>{ formatCurrency( total, order.currency_symbol ) }</span>
<span>{ formatCurrency( total ) }</span>
) }
</div>
}
actions={
<Button isDefault href={ getAdminLink( 'post.php?action=edit&post=' + order.id ) }>
<Button isDefault href={ getAdminLink( 'post.php?action=edit&post=' + order.order_id ) }>
{ __( 'Begin fulfillment' ) }
</Button>
}
@ -162,29 +188,30 @@ function OrdersPanel( { orders, isRequesting, isError } ) {
}
OrdersPanel.propTypes = {
orders: PropTypes.instanceOf( Map ).isRequired,
orders: PropTypes.array.isRequired,
isError: PropTypes.bool,
isRequesting: PropTypes.bool,
};
OrdersPanel.defaultProps = {
orders: new Map(),
orders: [],
isError: false,
isRequesting: false,
};
export default compose(
withSelect( select => {
const { getItems, getItemsError, isGetItemsRequesting } = select( 'wc-api' );
const { getReportItems, getReportItemsError, isReportItemsRequesting } = select( 'wc-api' );
const ordersQuery = {
page: 1,
per_page: QUERY_DEFAULTS.pageSize,
status: 'processing',
status_is: [ 'processing', 'on-hold' ],
extended_info: true,
};
const orders = getItems( 'orders', ordersQuery );
const isError = Boolean( getItemsError( 'orders', ordersQuery ) );
const isRequesting = isGetItemsRequesting( 'orders', ordersQuery );
const orders = getReportItems( 'orders', ordersQuery ).data;
const isError = Boolean( getReportItemsError( 'orders', ordersQuery ) );
const isRequesting = isReportItemsRequesting( 'orders', ordersQuery );
return { orders, isError, isRequesting };
} )

View File

@ -12,7 +12,7 @@ import PropTypes from 'prop-types';
/**
* WooCommerce dependencies
*/
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
import { getNewPath } from '@woocommerce/navigation';
import { Link } from '@woocommerce/components';
/**
@ -89,7 +89,7 @@ class Header extends Component {
{ _sections.map( ( section, i ) => {
const sectionPiece = Array.isArray( section ) ? (
<Link
href={ getNewPath( getPersistedQuery(), section[ 0 ], {} ) }
href={ getNewPath( {}, section[ 0 ], {} ) }
type={ isEmbedded ? 'wp-admin' : 'wc-admin' }
>
{ section[ 1 ] }

View File

@ -10,6 +10,7 @@ import { Provider as SlotFillProvider } from 'react-slot-fill';
*/
import './stylesheets/_index.scss';
import { PageLayout } from './layout';
import 'store';
import 'wc-api/wp-data-store';
render(

View File

@ -21,44 +21,52 @@ import Dashboard from 'dashboard';
import DevDocs from 'devdocs';
const getPages = () => {
const pages = [
{
container: Dashboard,
path: '/',
wpOpenMenu: 'toplevel_page_woocommerce',
wpClosedMenu: 'toplevel_page_wc-admin--analytics-revenue',
},
{
container: Analytics,
path: '/analytics',
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
wpClosedMenu: 'toplevel_page_woocommerce',
},
{
container: AnalyticsSettings,
path: '/analytics/settings',
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
wpClosedMenu: 'toplevel_page_woocommerce',
},
{
container: AnalyticsReport,
path: '/analytics/:report',
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
wpClosedMenu: 'toplevel_page_woocommerce',
},
{
const pages = [];
if ( window.wcAdminFeatures.devdocs ) {
pages.push( {
container: DevDocs,
path: '/devdocs',
wpOpenMenu: 'toplevel_page_woocommerce',
wpClosedMenu: 'toplevel_page_wc-admin--analytics-revenue',
},
{
} );
pages.push( {
container: DevDocs,
path: '/devdocs/:component',
wpOpenMenu: 'toplevel_page_woocommerce',
wpClosedMenu: 'toplevel_page_wc-admin--analytics-revenue',
},
];
} );
}
if ( window.wcAdminFeatures.dashboard ) {
pages.push( {
container: Dashboard,
path: '/',
wpOpenMenu: 'toplevel_page_woocommerce',
wpClosedMenu: 'toplevel_page_wc-admin--analytics-revenue',
} );
}
if ( window.wcAdminFeatures.analytics ) {
pages.push( {
container: Analytics,
path: '/analytics',
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
wpClosedMenu: 'toplevel_page_woocommerce',
} );
pages.push( {
container: AnalyticsSettings,
path: '/analytics/settings',
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
wpClosedMenu: 'toplevel_page_woocommerce',
} );
pages.push( {
container: AnalyticsReport,
path: '/analytics/:report',
wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue',
wpClosedMenu: 'toplevel_page_woocommerce',
} );
}
return pages;
};
@ -161,6 +169,9 @@ window.wpNavMenuClassChange = function( page ) {
closedMenu.classList.remove( 'wp-has-current-submenu' );
closedMenu.classList.remove( 'wp-menu-open' );
closedMenu.classList.add( 'wp-not-current-submenu' );
const wpWrap = document.querySelector( '#wpwrap' );
wpWrap.classList.remove( 'wp-responsive-open' );
};
export { Controller, getPages };

View File

@ -21,6 +21,7 @@ import { Controller, getPages } from './controller';
import Header from 'header';
import Notices from './notices';
import { recordPageView } from 'lib/tracks';
import TransientNotices from './transient-notices';
class Layout extends Component {
componentDidMount() {
@ -61,7 +62,8 @@ class Layout extends Component {
const { isEmbeded, ...restProps } = this.props;
return (
<div className="woocommerce-layout">
<Slot name="header" />
{ window.wcAdminFeatures[ 'activity-panels' ] && <Slot name="header" /> }
<TransientNotices />
<div className="woocommerce-layout__primary" id="woocommerce-layout__primary">
<Notices />

View File

@ -17,6 +17,10 @@
}
}
.woocommerce-feature-disabled-activity-panels .woocommerce-layout__primary {
margin-top: 20px;
}
.woocommerce-layout .woocommerce-layout__main {
padding-right: $fallback-gutter-large;
padding-right: $gutter-large;

View File

@ -0,0 +1,48 @@
/** @format */
/**
* External dependencies
*/
import classnames from 'classnames';
import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import './style.scss';
import TransientNotice from './transient-notice';
import withSelect from 'wc-api/with-select';
class TransientNotices extends Component {
render() {
const { className, notices } = this.props;
const classes = classnames( 'woocommerce-transient-notices', className );
return (
<section className={ classes }>
{ notices && notices.map( ( notice, i ) => <TransientNotice key={ i } { ...notice } /> ) }
</section>
);
}
}
TransientNotices.propTypes = {
/**
* Additional class name to style the component.
*/
className: PropTypes.string,
/**
* Array of notices to be displayed.
*/
notices: PropTypes.array,
};
export default compose(
withSelect( select => {
const { getNotices } = select( 'wc-admin' );
const notices = getNotices();
return { notices };
} )
)( TransientNotices );

View File

@ -0,0 +1,38 @@
/** @format */
.woocommerce-transient-notices {
position: fixed;
bottom: $gap-small;
left: 0;
z-index: 99999;
}
.woocommerce-transient-notice {
transform: translateX(calc(-100% - #{$gap}));
transition: all 300ms cubic-bezier(0.42, 0, 0.58, 1);
max-height: 300px; // Used to animate sliding down when multiple notices exist on exit.
@media screen and (prefers-reduced-motion: reduce) {
transition: none;
}
&.slide-enter-active,
&.slide-enter-done {
transform: translateX(0%);
}
&.slide-exit-active {
transform: translateX(calc(-100% - #{$gap}));
}
&.slide-exit-done {
max-height: 0;
margin: 0;
padding: 0;
visibility: hidden;
}
.components-notice {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
}
}

View File

@ -0,0 +1,104 @@
/** @format */
/**
* External dependencies
*/
import classnames from 'classnames';
import { Component } from '@wordpress/element';
import { CSSTransition } from 'react-transition-group';
import { noop } from 'lodash';
import { Notice } from '@wordpress/components';
import PropTypes from 'prop-types';
import { speak } from '@wordpress/a11y';
class TransientNotice extends Component {
constructor( props ) {
super( props );
this.state = {
visible: false,
timeout: null,
};
}
componentDidMount() {
const exitTime = this.props.exitTime;
const timeout = setTimeout(
() => {
this.setState( { visible: false } );
},
exitTime,
name,
exitTime
);
/* eslint-disable react/no-did-mount-set-state */
this.setState( { visible: true, timeout } );
/* eslint-enable react/no-did-mount-set-state */
speak( this.props.message );
}
componentWillUnmount() {
clearTimeout( this.state.timeout );
}
render() {
const { actions, className, isDismissible, message, onRemove, status } = this.props;
const classes = classnames( 'woocommerce-transient-notice', className );
return (
<CSSTransition in={ this.state.visible } timeout={ 300 } classNames="slide">
<div className={ classes }>
<Notice
status={ status }
isDismissible={ isDismissible }
onRemove={ onRemove }
actions={ actions }
>
{ message }
</Notice>
</div>
</CSSTransition>
);
}
}
TransientNotice.propTypes = {
/**
* Array of action objects.
* See https://wordpress.org/gutenberg/handbook/designers-developers/developers/components/notice/
*/
actions: PropTypes.array,
/**
* Additional class name to style the component.
*/
className: PropTypes.string,
/**
* Determines if the notice dimiss button should be shown.
* See https://wordpress.org/gutenberg/handbook/designers-developers/developers/components/notice/
*/
isDismissible: PropTypes.bool,
/**
* Function called when dismissing the notice.
* See https://wordpress.org/gutenberg/handbook/designers-developers/developers/components/notice/
*/
onRemove: PropTypes.func,
/**
* Type of notice to display.
* See https://wordpress.org/gutenberg/handbook/designers-developers/developers/components/notice/
*/
status: PropTypes.oneOf( [ 'success', 'error', 'warning' ] ),
/**
* Time in milliseconds until exit.
*/
exitTime: PropTypes.number,
};
TransientNotice.defaultProps = {
actions: [],
className: '',
exitTime: 7000,
isDismissible: false,
onRemove: noop,
status: 'warning',
};
export default TransientNotice;

View File

@ -0,0 +1,22 @@
/** @format */
/**
* External dependencies
*/
import { combineReducers, registerStore } from '@wordpress/data';
/**
* Internal dependencies
*/
import notices from './notices';
registerStore( 'wc-admin', {
reducer: combineReducers( {
...notices.reducers,
} ),
actions: {
...notices.actions,
},
selectors: {
...notices.selectors,
},
} );

View File

@ -0,0 +1,9 @@
/** @format */
const addNotice = notice => {
return { type: 'ADD_NOTICE', notice };
};
export default {
addNotice,
};

View File

@ -0,0 +1,13 @@
/** @format */
/**
* Internal dependencies
*/
import reducers from './reducers';
import actions from './actions';
import selectors from './selectors';
export default {
reducers,
actions,
selectors,
};

View File

@ -0,0 +1,15 @@
/** @format */
const DEFAULT_STATE = [];
const notices = ( state = DEFAULT_STATE, action ) => {
if ( action.type === 'ADD_NOTICE' ) {
return [ ...state, action.notice ];
}
return state;
};
export default {
notices,
};

View File

@ -0,0 +1,9 @@
/** @format */
const getNotices = state => {
return state.notices;
};
export default {
getNotices,
};

View File

@ -0,0 +1,7 @@
/** @format */
export const DEFAULT_STATE = {
notices: [],
};
export const testNotice = { message: 'Test notice' };

View File

@ -0,0 +1,55 @@
/** @format */
/**
* Internal dependencies
*/
import actions from '../actions';
import { DEFAULT_STATE, testNotice } from './fixtures';
import reducers from '../reducers';
import selectors from '../selectors';
describe( 'actions', () => {
test( 'should create an add notice action', () => {
const expectedAction = {
type: 'ADD_NOTICE',
notice: testNotice,
};
expect( actions.addNotice( testNotice ) ).toEqual( expectedAction );
} );
} );
describe( 'selectors', () => {
const notices = [ testNotice ];
const updatedState = { ...DEFAULT_STATE, ...{ notices } };
test( 'should return an emtpy initial state', () => {
expect( selectors.getNotices( DEFAULT_STATE ) ).toEqual( [] );
} );
test( 'should have an array length matching number of notices', () => {
expect( selectors.getNotices( updatedState ).length ).toEqual( 1 );
} );
test( 'should return the message content', () => {
expect( selectors.getNotices( updatedState )[ 0 ].message ).toEqual( 'Test notice' );
} );
} );
describe( 'reducers', () => {
test( 'should return an emtpy initial state', () => {
expect( reducers.notices( DEFAULT_STATE.notices, {} ) ).toEqual( [] );
} );
test( 'should return the added notice', () => {
expect(
reducers.notices( DEFAULT_STATE.notices, { type: 'ADD_NOTICE', notice: testNotice } )
).toEqual( [ testNotice ] );
} );
const initialNotices = [ { message: 'Initial notice' } ];
test( 'should return the initial notice and the added notice', () => {
expect(
reducers.notices( initialNotices, { type: 'ADD_NOTICE', notice: testNotice } )
).toEqual( [ ...initialNotices, testNotice ] );
} );
} );

View File

@ -59,3 +59,38 @@
}
}
}
// Hide an element from sighted users, but availble to screen reader users.
@mixin visually-hidden() {
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
/* Many screen reader and browser combinations announce broken words as they would appear visually. */
overflow-wrap: normal !important;
word-wrap: normal !important;
}
// Unhide a visually hidden element
@mixin visually-shown() {
clip: auto;
clip-path: none;
height: auto;
width: auto;
margin: unset;
overflow: hidden;
}
// Create a string-repeat function
@function str-repeat($character, $n) {
@if $n == 0 {
@return '';
}
$c: '';
@for $i from 1 through $n {
$c: $c + $character;
}
@return $c;
}

View File

@ -56,11 +56,13 @@
.components-button.is-button.is-primary {
color: $white;
}
.components-button.is-button.is-primary:hover,
.components-button.is-button.is-primary:active,
.components-button.is-button.is-primary:focus {
color: $white;
&:not(:disabled) {
&:hover,
&:active,
&:focus {
color: $white;
}
}
}
}

View File

@ -6,12 +6,9 @@ import { MINUTE } from '@fresh-data/framework';
export const NAMESPACE = '/wc/v4';
// @todo Remove once swagger endpoints are phased out.
export const SWAGGERNAMESPACE = 'https://virtserver.swaggerhub.com/peterfabian/wc-v3-api/1.0.0/';
export const DEFAULT_REQUIREMENT = {
timeout: 1 * MINUTE,
freshness: 5 * MINUTE,
freshness: 30 * MINUTE,
};
// WordPress & WooCommerce both set a hard limit of 100 for the per_page parameter

View File

@ -7,17 +7,16 @@
/**
* Returns items based on a search query.
*
* @param {Object} select Instance of @wordpress/select
* @param {String} endpoint Report API Endpoint
* @param {String} search Search strings separated by commas.
* @return {Object} Object Object containing the matching items.
* @param {Object} select Instance of @wordpress/select
* @param {String} endpoint Report API Endpoint
* @param {String[]} search Array of search strings.
* @return {Object} Object containing the matching items.
*/
export function searchItemsByString( select, endpoint, search ) {
const { getItems } = select( 'wc-api' );
const searchWords = search.split( ',' );
const items = {};
searchWords.forEach( searchWord => {
search.forEach( searchWord => {
const newItems = getItems( endpoint, {
search: searchWord,
per_page: 10,

View File

@ -13,7 +13,7 @@ import { stringifyQuery } from '@woocommerce/navigation';
* Internal dependencies
*/
import { getResourceIdentifier, getResourcePrefix } from 'wc-api/utils';
import { NAMESPACE, SWAGGERNAMESPACE } from 'wc-api/constants';
import { NAMESPACE } from 'wc-api/constants';
const statEndpoints = [
'coupons',
@ -21,11 +21,10 @@ const statEndpoints = [
'orders',
'products',
'revenue',
'stock',
'taxes',
'customers',
];
// @todo Remove once swagger endpoints are phased out.
const swaggerEndpoints = [ 'categories' ];
const typeEndpointMap = {
'report-stats-query-orders': 'orders',
@ -34,6 +33,7 @@ const typeEndpointMap = {
'report-stats-query-categories': 'categories',
'report-stats-query-downloads': 'downloads',
'report-stats-query-coupons': 'coupons',
'report-stats-query-stock': 'stock',
'report-stats-query-taxes': 'taxes',
'report-stats-query-customers': 'customers',
};
@ -53,9 +53,7 @@ function read( resourceNames, fetch = apiFetch ) {
parse: false,
};
if ( swaggerEndpoints.indexOf( endpoint ) >= 0 ) {
fetchArgs.url = SWAGGERNAMESPACE + 'reports/' + endpoint + '/stats' + stringifyQuery( query );
} else if ( statEndpoints.indexOf( endpoint ) >= 0 ) {
if ( statEndpoints.indexOf( endpoint ) >= 0 ) {
fetchArgs.path = NAMESPACE + '/reports/' + endpoint + '/stats' + stringifyQuery( query );
} else {
fetchArgs.path = endpoint + stringifyQuery( query );

View File

@ -3,7 +3,7 @@
/**
* External dependencies
*/
import { find, forEach, isNull, get } from 'lodash';
import { find, forEach, isNull, get, includes } from 'lodash';
import moment from 'moment';
/**
@ -52,6 +52,9 @@ export function getFilterQuery( endpoint, query ) {
return {};
}
// Some stats endpoints don't have interval data, so they can ignore after/before params and omit that part of the response.
const noIntervalEndpoints = [ 'stock', 'customers' ];
/**
* Add timestamp to advanced filter parameters involving date. The api
* expects a timestamp for these values similar to `before` and `after`.
@ -136,10 +139,11 @@ export function getQueryFromConfig( config, advancedFilters, query ) {
/**
* Returns true if a report object is empty.
*
* @param {Object} report Report to check
* @param {Object} report Report to check
* @param {String} endpoint Endpoint slug
* @return {Boolean} True if report is data is empty.
*/
export function isReportDataEmpty( report ) {
export function isReportDataEmpty( report, endpoint ) {
if ( ! report ) {
return true;
}
@ -149,7 +153,9 @@ export function isReportDataEmpty( report ) {
if ( ! report.data.totals || isNull( report.data.totals ) ) {
return true;
}
if ( ! report.data.intervals || 0 === report.data.intervals.length ) {
const checkIntervals = ! includes( noIntervalEndpoints, endpoint );
if ( checkIntervals && ( ! report.data.intervals || 0 === report.data.intervals.length ) ) {
return true;
}
return false;
@ -168,15 +174,19 @@ function getRequestQuery( endpoint, dataType, query ) {
const interval = getIntervalForQuery( query );
const filterQuery = getFilterQuery( endpoint, query );
const end = datesFromQuery[ dataType ].before;
return {
order: 'asc',
interval,
per_page: MAX_PER_PAGE,
after: appendTimestamp( datesFromQuery[ dataType ].after, 'start' ),
before: appendTimestamp( end, 'end' ),
segmentby: query.segmentby,
...filterQuery,
};
const noIntervals = includes( noIntervalEndpoints, endpoint );
return noIntervals
? { ...filterQuery }
: {
order: 'asc',
interval,
per_page: MAX_PER_PAGE,
after: appendTimestamp( datesFromQuery[ dataType ].after, 'start' ),
before: appendTimestamp( end, 'end' ),
segmentby: query.segmentby,
...filterQuery,
};
}
/**
@ -250,7 +260,7 @@ export function getReportChartData( endpoint, dataType, query, select ) {
return { ...response, isRequesting: true };
} else if ( getReportStatsError( endpoint, requestQuery ) ) {
return { ...response, isError: true };
} else if ( isReportDataEmpty( stats ) ) {
} else if ( isReportDataEmpty( stats, endpoint ) ) {
return { ...response, isEmpty: true };
}

View File

@ -48,13 +48,13 @@ function updateSettings( resourceNames, data, fetch ) {
method: 'POST',
data: { value: settingsData[ setting ] },
} )
.then( settingsToSettingsResource )
.then( settingToSettingsResource.bind( null, data.settings ) )
.catch( error => {
return { [ resourceName ]: { error } };
} );
} );
return [ promises ];
return promises;
}
return [];
}
@ -65,6 +65,11 @@ function settingsToSettingsResource( settings ) {
return { [ 'settings' ]: { data: settingsData } };
}
function settingToSettingsResource( settings, setting ) {
settings[ setting.id ] = setting.value;
return { [ 'settings' ]: { data: settings } };
}
export default {
read,
update,

View File

@ -1,14 +1,34 @@
/** @format */
/**
* External dependencies
*/
import { isNil } from 'lodash';
/**
* Internal dependencies
*/
import { DEFAULT_REQUIREMENT } from '../constants';
const getSettings = ( getResource, requireResource ) => ( requirement = DEFAULT_REQUIREMENT ) => {
return requireResource( requirement, 'settings' ).data;
return requireResource( requirement, 'settings' ).data || {};
};
const getSettingsError = getResource => () => {
return getResource( 'settings' ).error;
};
const isGetSettingsRequesting = getResource => () => {
const { lastRequested, lastReceived } = getResource( 'settings' );
if ( isNil( lastRequested ) || isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
export default {
getSettings,
getSettingsError,
isGetSettingsRequesting,
};

View File

@ -0,0 +1,8 @@
{
"features": {
"activity-panels": false,
"analytics": false,
"dashboard": false,
"devdocs": false
}
}

View File

@ -0,0 +1,8 @@
{
"features": {
"activity-panels": true,
"analytics": true,
"dashboard": true,
"devdocs": true
}
}

View File

@ -0,0 +1,8 @@
{
"features": {
"activity-panels": false,
"analytics": true,
"dashboard": true,
"devdocs": false
}
}

View File

@ -1,5 +1,6 @@
* [Overview](/)
* [Components](components/)
* [Feature Flags](feature-flags)
* [Data](data)
* [Documentation](documentation)
* [Layout](layout)

View File

@ -26,6 +26,7 @@
* [Pagination](components/packages/pagination.md)
* [ProductImage](components/packages/product-image.md)
* [Rating](components/packages/rating.md)
* [SearchListControl](components/packages/search-list-control.md)
* [Search](components/packages/search.md)
* [SectionHeader](components/packages/section-header.md)
* [Section](components/packages/section.md)

View File

@ -38,9 +38,14 @@ Current path
### `primaryData`
- **Required**
- Type: Object
- Default: null
- Default: `{
data: {
intervals: [],
},
isError: false,
isRequesting: false,
}`
Primary data to display in the chart.
@ -54,9 +59,14 @@ The query string represented in object form.
### `secondaryData`
- **Required**
- Type: Object
- Default: null
- Default: `{
data: {
intervals: [],
},
isError: false,
isRequesting: false,
}`
Secondary data to display in the chart.

View File

@ -42,3 +42,17 @@ The query string represented in object form.
Properties of the selected chart.
### `summaryData`
- Type: Object
- Default: `{
totals: {
primary: {},
secondary: {},
},
isError: false,
isRequesting: false,
}`
Data to display in the SummaryNumbers.

View File

@ -68,9 +68,8 @@ The name of the property in the item object which contains the id.
### `primaryData`
- **Required**
- Type: Object
- Default: null
- Default: `{}`
Primary data of that report. If it's not provided, it will be automatically
loaded via the provided `endpoint`.
@ -78,7 +77,13 @@ loaded via the provided `endpoint`.
### `tableData`
- Type: Object
- Default: `{}`
- Default: `{
items: {
data: [],
totalResults: 0,
},
query: {},
}`
Table data of that report. If it's not provided, it will be automatically
loaded via the provided `endpoint`.

View File

@ -13,6 +13,21 @@ Props
Allowed intervals to show in a dropdown.
### `baseValue`
- Type: Number
- Default: `0`
Base chart value. If no data value is different than the baseValue, the
`emptyMessage` will be displayed if provided.
### `chartType`
- Type: One of: 'bar', 'line'
- Default: `'line'`
Chart type of either `line` or `bar`.
### `data`
- Type: Array
@ -27,6 +42,14 @@ An array of data.
Format to parse dates into d3 time format
### `emptyMessage`
- Type: String
- Default: null
The message to be displayed if there is no data to render. If no message is provided,
nothing will be displayed.
### `itemsLabel`
- Type: String
@ -127,13 +150,6 @@ A number formatting string or function to format the value displayed in the tool
A string to use as a title for the tooltip. Takes preference over `tooltipLabelFormat`.
### `type`
- Type: One of: 'bar', 'line'
- Default: `'line'`
Chart type of either `line` or `bar`.
### `valueType`
- Type: String

View File

@ -0,0 +1,176 @@
`SearchListControl` (component)
===============================
Component to display a searchable, selectable list of items.
Props
-----
### `className`
- Type: String
- Default: null
Additional CSS classes.
### `isHierarchical`
- Type: Boolean
- Default: null
Whether the list of items is hierarchical or not. If true, each list item is expected to
have a parent property.
### `isLoading`
- Type: Boolean
- Default: null
Whether the list of items is still loading.
### `isSingle`
- Type: Boolean
- Default: null
Restrict selections to one item.
### `list`
- Type: Array
- id: Number
- name: String
- Default: null
A complete list of item objects, each with id, name properties. This is displayed as a
clickable/keyboard-able list, and possibly filtered by the search term (searches name).
### `messages`
- Type: Object
- clear: String - A more detailed label for the "Clear all" button, read to screen reader users.
- list: String - Label for the list of selectable items, only read to screen reader users.
- noItems: String - Message to display when the list is empty (implies nothing loaded from the server
or parent component).
- noResults: String - Message to display when no matching results are found. %s is the search term.
- search: String - Label for the search input
- selected: Function - Label for the selected items. This is actually a function, so that we can pass
through the count of currently selected items.
- updated: String - Label indicating that search results have changed, read to screen reader users.
- Default: null
Messages displayed or read to the user. Configure these to reflect your object type.
See `defaultMessages` above for examples.
### `onChange`
- **Required**
- Type: Function
- Default: null
Callback fired when selected items change, whether added, cleared, or removed.
Passed an array of item objects (as passed in via props.list).
### `renderItem`
- Type: Function
- Default: null
Callback to render each item in the selection list, allows any custom object-type rendering.
### `selected`
- **Required**
- Type: Array
- Default: null
The list of currently selected items.
### `search`
- Type: String
- Default: null
### `setState`
- Type: Function
- Default: null
### `debouncedSpeak`
- Type: Function
- Default: null
### `instanceId`
- Type: Number
- Default: null
`SearchListItem` (component)
============================
Props
-----
### `className`
- Type: String
- Default: null
Additional CSS classes.
### `depth`
- Type: Number
- Default: `0`
Depth, non-zero if the list is hierarchical.
### `item`
- Type: Object
- Default: null
Current item to display.
### `isSelected`
- Type: Boolean
- Default: null
Whether this item is selected.
### `isSingle`
- Type: Boolean
- Default: null
Whether this should only display a single item (controls radio vs checkbox icon).
### `onSelect`
- Type: Function
- Default: null
Callback for selecting the item.
### `search`
- Type: String
- Default: `''`
Search string, used to highlight the substring in the item name.
### `showCount`
- Type: Boolean
- Default: `false`
Toggles the "count" bubble on/off.

View File

@ -7,6 +7,13 @@ A search box which autocompletes results while typing, allowing for the user to
Props
-----
### `allowFreeTextSearch`
- Type: Boolean
- Default: `false`
Render additional options in the autocompleter to allow free text entering depending on the type.
### `className`
- Type: String
@ -54,6 +61,13 @@ search box.
Render tags inside input, otherwise render below input.
### `showClearButton`
- Type: Boolean
- Default: `false`
Render a 'Clear' button next to the input box to remove its contents.
### `staticResults`
- Type: Boolean

View File

@ -132,13 +132,6 @@ The total number of rows to display per page.
The string to use as a query parameter when searching row items.
### `searchParam`
- Type: String
- Default: null
Url query parameter search function operates on
### `showMenu`
- Type: Boolean

View File

@ -0,0 +1,47 @@
# Feature Flags
Features inside the `wc-admin` repository can be in various states of completeness. In addition to the development copy of `wc-admin`, feature plugin versions are bundled, and code is merged to WooCommerce core. To provide a way for improved control over how these features are released in these different environments, `wc-admin` has a system for feature flags.
We currently support the following environments:
| Environment | Description |
|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| development | Development - All features should be enabled in development. These flags are also used in both JS and PHP tests. Ran using `npm start`. |
| plugin | Plugin - A packaged release of the featured plugin, for GitHub WordPress.org. Ran using `npm run-script build:release`. | |
| core | Core - assets/files ready and stable enough for core merge. @todo No build process exists yet.
## Adding a new flag
Flags can be added to the files located in the `config/` directory. Make sure to add a flag for each environment and explicitly set the flag to false.
Please add new feature flags alphabetically so they are easy to find.
## Basic Use - Client
The `window.wcAdminFeatures` constant is a global variable containing the feature flags.
Instances of `window.wcAdminFeatures` are replaced during the webpack build process by using webpack's [define plugin](https://webpack.js.org/plugins/define-plugin/). Using `webpack` for this allows us to eliminate dead code when making minified/production builds (`plugin`, or `core` environments).
To check if a feature is enabled, you can simplify check the boolean value of a feature:
```
{ window.wcAdminFeatures[ 'activity-panels' ] && <ActivityPanel /> }
```
We also expose CSS classes on the `body` tag, so that you can target specific feature states:
```
<body class="wp-admin woocommerce-page woocommerce-feature-disabled-analytics woocommerce-feature-enabled-activity-panels ....">
```
## Basic Use - Server
Feature flags are also available via PHP. To ensure these are consistent with the built client assets, `includes/feature-flags.php` is generated by the plugin build process or `npm start`. Do not edit `includes/feature-flags.php` directly.
To check if a feature is enabled, you can use the `wc_admin_is_feature_enabled()`:
```
if ( wc_admin_is_feature_enabled( 'activity-panels' ) ) {
add_action( 'admin_header', 'wc_admin_activity_panel' );
}
```

View File

@ -13,32 +13,27 @@ defined( 'ABSPATH' ) || exit;
* Customers controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Customers_Controller
* @extends WC_Admin_REST_Reports_Customers_Controller
*/
class WC_Admin_REST_Customers_Controller extends WC_REST_Customers_Controller {
// @todo Add support for guests here. See https://wp.me/p7bje6-1dM.
class WC_Admin_REST_Customers_Controller extends WC_Admin_REST_Reports_Customers_Controller {
/**
* Endpoint namespace.
* Route base.
*
* @var string
*/
protected $namespace = 'wc/v4';
protected $rest_base = 'customers';
/**
* Searches emails by partial search instead of a strict match.
* See "search parameters" under https://codex.wordpress.org/Class_Reference/WP_User_Query.
* Maps query arguments from the REST request.
*
* @param array $prepared_args Prepared search filter args from the customer endpoint.
* @param array $request Request/query arguments.
* @param array $request Request array.
* @return array
*/
public static function update_search_filters( $prepared_args, $request ) {
if ( ! empty( $request['email'] ) ) {
$prepared_args['search'] = '*' . $prepared_args['search'] . '*';
}
return $prepared_args;
protected function prepare_reports_query( $request ) {
$args = parent::prepare_reports_query( $request );
$args['customers'] = $request['include'];
return $args;
}
/**
@ -48,11 +43,8 @@ class WC_Admin_REST_Customers_Controller extends WC_REST_Customers_Controller {
*/
public function get_collection_params() {
$params = parent::get_collection_params();
// Allow partial email matches. Previously, this was of format 'email' which required a strict "test@example.com" format.
// This, in combination with `update_search_filters` allows us to do partial searches.
$params['email']['format'] = '';
$params['include'] = $params['customers'];
unset( $params['customers'] );
return $params;
}
}
add_filter( 'woocommerce_rest_customer_query', array( 'WC_Admin_REST_Customers_Controller', 'update_search_filters' ), 10, 2 );

View File

@ -46,7 +46,8 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
$args['order'] = $request['order'];
$args['orderby'] = $request['orderby'];
$args['match'] = $request['match'];
$args['name'] = $request['name'];
$args['search'] = $request['search'];
$args['searchby'] = $request['searchby'];
$args['username'] = $request['username'];
$args['email'] = $request['email'];
$args['country'] = $request['country'];
@ -60,6 +61,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
$args['avg_order_value_max'] = $request['avg_order_value_max'];
$args['last_order_before'] = $request['last_order_before'];
$args['last_order_after'] = $request['last_order_after'];
$args['customers'] = $request['customers'];
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
$normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
@ -172,7 +174,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
'title' => 'report_customers',
'type' => 'object',
'properties' => array(
'customer_id' => array(
'id' => array(
'description' => __( 'Customer ID.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
@ -333,11 +335,21 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['name'] = array(
'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ),
$params['search'] = array(
'description' => __( 'Limit response to objects with a customer field containing the search term. Searches the field provided by `searchby`.', 'wc-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['searchby'] = array(
'description' => 'Limit results with `search` and `searchby` to specific fields containing the search term.',
'type' => 'string',
'default' => 'name',
'enum' => array(
'name',
'username',
'email',
),
);
$params['username'] = array(
'description' => __( 'Limit response to objects with a specfic username.', 'wc-admin' ),
'type' => 'string',
@ -446,6 +458,16 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['customers'] = array(
'description' => __( 'Limit result to items with specified customer ids.', 'wc-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
return $params;
}
}

View File

@ -41,7 +41,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
$args['registered_before'] = $request['registered_before'];
$args['registered_after'] = $request['registered_after'];
$args['match'] = $request['match'];
$args['name'] = $request['name'];
$args['search'] = $request['search'];
$args['username'] = $request['username'];
$args['email'] = $request['email'];
$args['country'] = $request['country'];
@ -55,6 +55,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
$args['avg_order_value_max'] = $request['avg_order_value_max'];
$args['last_order_before'] = $request['last_order_before'];
$args['last_order_after'] = $request['last_order_after'];
$args['customers'] = $request['customers'];
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
$normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
@ -77,8 +78,6 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
$report_data = $customers_query->get_data();
$out_data = array(
'totals' => $report_data,
// @todo Is this needed? the single element array tricks the isReportDataEmpty() selector.
'intervals' => array( (object) array() ),
);
return rest_ensure_response( $out_data );
@ -161,55 +160,6 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
'readonly' => true,
'properties' => $totals,
),
'intervals' => array( // @todo Remove this?
'description' => __( 'Reports data grouped by intervals.', 'wc-admin' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'interval' => array(
'description' => __( 'Type of interval.', 'wc-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.", 'wc-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_start_gmt' => array(
'description' => __( 'The date the report start, as GMT.', 'wc-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end' => array(
'description' => __( "The date the report end, in the site's timezone.", 'wc-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end_gmt' => array(
'description' => __( 'The date the report end, as GMT.', 'wc-admin' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'wc-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
),
),
),
),
);
@ -246,11 +196,21 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['name'] = array(
'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ),
$params['search'] = array(
'description' => __( 'Limit response to objects with a customer field containing the search term. Searches the field provided by `searchby`.', 'wc-admin' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['searchby'] = array(
'description' => 'Limit results with `search` and `searchby` to specific fields containing the search term.',
'type' => 'string',
'default' => 'name',
'enum' => array(
'name',
'username',
'email',
),
);
$params['username'] = array(
'description' => __( 'Limit response to objects with a specfic username.', 'wc-admin' ),
'type' => 'string',
@ -359,6 +319,15 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['customers'] = array(
'description' => __( 'Limit result to items with specified customer ids.', 'wc-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
return $params;
}

View File

@ -35,7 +35,6 @@ class WC_Admin_REST_Reports_Downloads_Controller extends WC_REST_Reports_Control
* Get items.
*
* @param WP_REST_Request $request Request data.
*
* @return array|WP_Error
*/
public function get_items( $request ) {
@ -111,6 +110,8 @@ class WC_Admin_REST_Reports_Downloads_Controller extends WC_REST_Reports_Control
$filename = basename( $file_path );
$response->data['file_name'] = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id );
$response->data['file_path'] = $file_path;
$customer = new WC_Customer( $data['user_id'] );
$response->data['username'] = $customer->get_username();
/**
* Filter a report returned from the API.
@ -136,10 +137,6 @@ class WC_Admin_REST_Reports_Downloads_Controller extends WC_REST_Reports_Control
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, 'products', $object['product_id'] ) ),
'embeddable' => true,
),
'user' => array(
'href' => rest_url( 'wp/v2/users/' . $object['user_id'] ),
'embeddable' => true,
),
);
return $links;
@ -216,6 +213,12 @@ class WC_Admin_REST_Reports_Downloads_Controller extends WC_REST_Reports_Control
'context' => array( 'view', 'edit' ),
'description' => __( 'User ID for the downloader.', 'wc-admin' ),
),
'username' => array(
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'User name of the downloader.', 'wc-admin' ),
),
'ip_address' => array(
'type' => 'string',
'readonly' => true,
@ -330,7 +333,7 @@ class WC_Admin_REST_Reports_Downloads_Controller extends WC_REST_Reports_Control
'type' => 'integer',
),
);
$params['user_includes'] = array(
$params['customer_includes'] = array(
'description' => __( 'Limit response to objects that have the specified user ids.', 'wc-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
@ -339,7 +342,7 @@ class WC_Admin_REST_Reports_Downloads_Controller extends WC_REST_Reports_Control
'type' => 'integer',
),
);
$params['user_excludes'] = array(
$params['customer_excludes'] = array(
'description' => __( 'Limit response to objects that don\'t have the specified user ids.', 'wc-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',

View File

@ -49,6 +49,8 @@ class WC_Admin_REST_Reports_Downloads_Stats_Controller extends WC_REST_Reports_C
$args['match'] = $request['match'];
$args['product_includes'] = (array) $request['product_includes'];
$args['product_excludes'] = (array) $request['product_excludes'];
$args['customer_includes'] = (array) $request['customer_includes'];
$args['customer_excludes'] = (array) $request['customer_excludes'];
$args['order_includes'] = (array) $request['order_includes'];
$args['order_excludes'] = (array) $request['order_excludes'];
$args['ip_address_includes'] = (array) $request['ip_address_includes'];
@ -329,8 +331,8 @@ class WC_Admin_REST_Reports_Downloads_Stats_Controller extends WC_REST_Reports_C
'type' => 'integer',
),
);
$params['user_includes'] = array(
'description' => __( 'Limit response to objects that have the specified user ids.', 'wc-admin' ),
$params['customer_includes'] = array(
'description' => __( 'Limit response to objects that have the specified customer ids.', 'wc-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
@ -338,8 +340,8 @@ class WC_Admin_REST_Reports_Downloads_Stats_Controller extends WC_REST_Reports_C
'type' => 'integer',
),
);
$params['user_excludes'] = array(
'description' => __( 'Limit response to objects that don\'t have the specified user ids.', 'wc-admin' ),
$params['customer_excludes'] = array(
'description' => __( 'Limit response to objects that don\'t have the specified customer ids.', 'wc-admin' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',

View File

@ -170,7 +170,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
),
'avg_items_per_order' => array(
'description' => __( 'Average items per order', 'wc-admin' ),
'type' => 'integer',
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),

View File

@ -0,0 +1,138 @@
<?php
/**
* REST API Reports stock stats controller
*
* Handles requests to the /reports/stock/stats endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports stock stats controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Stock_Stats_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v4';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/stock/stats';
/**
* Get Stock Status Totals.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$stock_query = new WC_Admin_Reports_Stock_Stats_Query();
$report_data = $stock_query->get_data();
$out_data = array(
'totals' => $report_data,
);
return rest_ensure_response( $out_data );
}
/**
* Prepare a report object for serialization.
*
* @param WC_Product $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 WC_Product $product The original bject.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_stock_stats', $response, $product, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$totals = array(
'products' => array(
'description' => __( 'Number of products.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'lowstock' => array(
'description' => __( 'Number of low stock products.', 'wc-admin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
$status_options = wc_get_product_stock_status_options();
foreach ( $status_options as $status => $label ) {
$totals[ $status ] = array(
/* translators: Stock status. Example: "Number of low stock products */
'description' => sprintf( __( 'Number of %s products.', 'wc-admin' ), $label ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
);
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_customers_stats',
'type' => 'object',
'properties' => array(
'totals' => array(
'description' => __( 'Totals data.', 'wc-admin' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
return $params;
}
}

View File

@ -12,43 +12,6 @@ defined( 'ABSPATH' ) || exit;
*/
class WC_Admin_Api_Init {
/**
* Action hook for reducing a range of batches down to single actions.
*/
const QUEUE_BATCH_ACTION = 'wc-admin_queue_batches';
/**
* Action hook for queuing an action after another is complete.
*/
const QUEUE_DEPEDENT_ACTION = 'wc-admin_queue_dependent_action';
/**
* Action hook for processing a batch of customers.
*/
const CUSTOMERS_BATCH_ACTION = 'wc-admin_process_customers_batch';
/**
* Action hook for processing a batch of orders.
*/
const ORDERS_BATCH_ACTION = 'wc-admin_process_orders_batch';
/**
* Action hook for initializing the orders lookup batch creation.
*/
const ORDERS_LOOKUP_BATCH_INIT = 'wc-admin_orders_lookup_batch_init';
/**
* Action hook for processing a batch of orders.
*/
const SINGLE_ORDER_ACTION = 'wc-admin_process_order';
/**
* Queue instance.
*
* @var WC_Queue_Interface
*/
protected static $queue = null;
/**
* Boostrap REST API.
*/
@ -57,154 +20,121 @@ class WC_Admin_Api_Init {
add_action( 'plugins_loaded', array( $this, 'init_classes' ), 19 );
// Hook in data stores.
add_filter( 'woocommerce_data_stores', array( 'WC_Admin_Api_Init', 'add_data_stores' ) );
// Add wc-admin report tables to list of WooCommerce tables.
add_filter( 'woocommerce_install_get_tables', array( 'WC_Admin_Api_Init', 'add_tables' ) );
// REST API extensions init.
add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
add_filter( 'rest_endpoints', array( 'WC_Admin_Api_Init', 'filter_rest_endpoints' ), 10, 1 );
add_filter( 'woocommerce_debug_tools', array( 'WC_Admin_Api_Init', 'add_regenerate_tool' ) );
// Initialize syncing hooks.
add_action( 'wp_loaded', array( __CLASS__, 'orders_lookup_update_init' ) );
// Initialize scheduled action handlers.
add_action( self::QUEUE_BATCH_ACTION, array( __CLASS__, 'queue_batches' ), 10, 3 );
add_action( self::QUEUE_DEPEDENT_ACTION, array( __CLASS__, 'queue_dependent_action' ), 10, 3 );
add_action( self::CUSTOMERS_BATCH_ACTION, array( __CLASS__, 'customer_lookup_process_batch' ) );
add_action( self::ORDERS_BATCH_ACTION, array( __CLASS__, 'orders_lookup_process_batch' ) );
add_action( self::ORDERS_LOOKUP_BATCH_INIT, array( __CLASS__, 'orders_lookup_batch_init' ) );
add_action( self::SINGLE_ORDER_ACTION, array( __CLASS__, 'orders_lookup_process_order' ) );
// Add currency symbol to orders endpoint response.
add_filter( 'woocommerce_rest_prepare_shop_order_object', array( __CLASS__, 'add_currency_symbol_to_order_response' ) );
}
/**
* Get queue instance.
*
* @return WC_Queue_Interface
*/
public static function queue() {
if ( is_null( self::$queue ) ) {
self::$queue = WC()->queue();
}
return self::$queue;
}
/**
* Set queue instance.
*
* @param WC_Queue_Interface $queue Queue instance.
*/
public static function set_queue( $queue ) {
self::$queue = $queue;
}
/**
* Init classes.
*/
public function init_classes() {
// Interfaces.
require_once dirname( __FILE__ ) . '/interfaces/class-wc-admin-reports-data-store-interface.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/interfaces/class-wc-admin-reports-data-store-interface.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-query.php';
// Common date time code.
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-interval.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-interval.php';
// Exceptions.
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-parameter-exception.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-parameter-exception.php';
// WC Class extensions.
require_once dirname( __FILE__ ) . '/class-wc-admin-order.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-order.php';
// Segmentation.
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-segmenting.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-orders-stats-segmenting.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-products-stats-segmenting.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-stats-segmenting.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-taxes-stats-segmenting.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-segmenting.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-orders-stats-segmenting.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-products-stats-segmenting.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-coupons-stats-segmenting.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-taxes-stats-segmenting.php';
// Query classes for reports.
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-revenue-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-orders-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-orders-stats-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-products-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-variations-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-products-stats-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-categories-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-taxes-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-taxes-stats-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-stats-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-downloads-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-downloads-stats-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-customers-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-customers-stats-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-revenue-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-orders-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-orders-stats-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-products-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-variations-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-products-stats-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-categories-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-taxes-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-taxes-stats-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-coupons-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-coupons-stats-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-downloads-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-downloads-stats-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-customers-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-customers-stats-query.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-reports-stock-stats-query.php';
// Data stores.
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-orders-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-orders-stats-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-products-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-variations-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-products-stats-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-categories-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-taxes-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-taxes-stats-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-coupons-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-coupons-stats-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-downloads-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-downloads-stats-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-customers-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-customers-stats-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-orders-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-orders-stats-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-products-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-variations-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-products-stats-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-categories-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-taxes-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-taxes-stats-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-coupons-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-coupons-stats-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-downloads-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-downloads-stats-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-customers-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-customers-stats-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-reports-stock-stats-data-store.php';
// Data triggers.
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-notes-data-store.php';
require_once WC_ADMIN_ABSPATH . '/includes/data-stores/class-wc-admin-notes-data-store.php';
// CRUD classes.
require_once dirname( __FILE__ ) . '/class-wc-admin-note.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-notes.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-note.php';
require_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-notes.php';
}
/**
* Init REST API.
*/
public function rest_api_init() {
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-admin-notes-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-coupons-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-customers-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-countries-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-download-ips-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-orders-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-products-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-categories-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-variations-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-reviews-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-variations-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-setting-options-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-system-status-tools-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-categories-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-coupons-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-coupons-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-customers-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-customers-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-downloads-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-downloads-files-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-downloads-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-orders-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-orders-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-products-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-variations-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-products-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-performance-indicators-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-revenue-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-taxes-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-admin-notes-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-coupons-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-data-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-data-countries-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-data-download-ips-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-orders-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-products-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-product-categories-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-product-variations-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-product-reviews-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-product-variations-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-setting-options-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-system-status-tools-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-categories-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-coupons-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-coupons-stats-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-customers-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-customers-stats-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-downloads-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-downloads-files-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-downloads-stats-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-orders-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-products-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-variations-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-products-stats-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-performance-indicators-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-revenue-stats-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-taxes-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-taxes-stats-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-stock-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-reports-stock-stats-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-taxes-controller.php';
require_once WC_ADMIN_ABSPATH . '/includes/api/class-wc-admin-rest-customers-controller.php';
$controllers = apply_filters(
'woocommerce_admin_rest_controllers',
@ -236,6 +166,7 @@ class WC_Admin_Api_Init {
'WC_Admin_REST_Reports_Coupons_Controller',
'WC_Admin_REST_Reports_Coupons_Stats_Controller',
'WC_Admin_REST_Reports_Stock_Controller',
'WC_Admin_REST_Reports_Stock_Stats_Controller',
'WC_Admin_REST_Reports_Downloads_Controller',
'WC_Admin_REST_Reports_Downloads_Stats_Controller',
'WC_Admin_REST_Reports_Customers_Controller',
@ -429,323 +360,6 @@ class WC_Admin_Api_Init {
return $endpoints;
}
/**
* Regenerate data for reports.
*/
public static function regenerate_report_data() {
// Add registered customers to the lookup table before updating order stats
// so that the orders can be associated with the `customer_id` column.
self::customer_lookup_batch_init();
// Queue orders lookup to occur after customers lookup generation is done.
self::queue_dependent_action( self::ORDERS_LOOKUP_BATCH_INIT, array(), self::CUSTOMERS_BATCH_ACTION );
}
/**
* Adds regenerate tool.
*
* @param array $tools List of tools.
* @return array
*/
public static function add_regenerate_tool( $tools ) {
return array_merge(
$tools,
array(
'rebuild_stats' => array(
'name' => __( 'Rebuild reports data', 'wc-admin' ),
'button' => __( 'Rebuild reports', 'wc-admin' ),
'desc' => __( 'This tool will rebuild all of the information used by the reports.', 'wc-admin' ),
'callback' => array( 'WC_Admin_Api_Init', 'regenerate_report_data' ),
),
)
);
}
/**
* Schedule an action to process a single Order.
*
* @param int $order_id Order ID.
* @return void
*/
public static function schedule_single_order_process( $order_id ) {
if ( 'shop_order' !== get_post_type( $order_id ) ) {
return;
}
// This can get called multiple times for a single order, so we look
// for existing pending jobs for the same order to avoid duplicating efforts.
$existing_jobs = self::queue()->search(
array(
'status' => 'pending',
'per_page' => 1,
'claimed' => false,
'search' => "[{$order_id}]",
)
);
if ( $existing_jobs ) {
$existing_job = current( $existing_jobs );
// Bail out if there's a pending single order action, or a pending dependent action.
if (
( self::SINGLE_ORDER_ACTION === $existing_job->get_hook() ) ||
(
self::QUEUE_DEPEDENT_ACTION === $existing_job->get_hook() &&
in_array( self::SINGLE_ORDER_ACTION, $existing_job->get_args() )
)
) {
return;
}
}
// We want to ensure that customer lookup updates are scheduled before order updates.
self::queue_dependent_action( self::SINGLE_ORDER_ACTION, array( $order_id ), self::CUSTOMERS_BATCH_ACTION );
}
/**
* Attach order lookup update hooks.
*/
public static function orders_lookup_update_init() {
// Activate WC_Order extension.
WC_Admin_Order::add_filters();
add_action( 'save_post_shop_order', array( __CLASS__, 'schedule_single_order_process' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'schedule_single_order_process' ) );
WC_Admin_Reports_Orders_Stats_Data_Store::init();
WC_Admin_Reports_Customers_Data_Store::init();
WC_Admin_Reports_Coupons_Data_Store::init();
WC_Admin_Reports_Products_Data_Store::init();
WC_Admin_Reports_Taxes_Data_Store::init();
}
/**
* Init order/product lookup tables update (in batches).
*/
public static function orders_lookup_batch_init() {
$batch_size = self::get_batch_size( self::ORDERS_BATCH_ACTION );
$order_query = new WC_Order_Query(
array(
'return' => 'ids',
'limit' => 1,
'paginate' => true,
)
);
$result = $order_query->get_orders();
if ( 0 === $result->total ) {
return;
}
$num_batches = ceil( $result->total / $batch_size );
self::queue_batches( 1, $num_batches, self::ORDERS_BATCH_ACTION );
}
/**
* Process a batch of orders to update (stats and products).
*
* @param int $batch_number Batch number to process (essentially a query page number).
* @return void
*/
public static function orders_lookup_process_batch( $batch_number ) {
$batch_size = self::get_batch_size( self::ORDERS_BATCH_ACTION );
$order_query = new WC_Order_Query(
array(
'return' => 'ids',
'limit' => $batch_size,
'page' => $batch_number,
'orderby' => 'ID',
'order' => 'ASC',
)
);
$order_ids = $order_query->get_orders();
foreach ( $order_ids as $order_id ) {
self::orders_lookup_process_order( $order_id );
}
}
/**
* Process a single order to update lookup tables for.
* If an error is encountered in one of the updates, a retry action is scheduled.
*
* @param int $order_id Order ID.
* @return void
*/
public static function orders_lookup_process_order( $order_id ) {
$result = array_sum(
array(
WC_Admin_Reports_Orders_Stats_Data_Store::sync_order( $order_id ),
WC_Admin_Reports_Products_Data_Store::sync_order_products( $order_id ),
WC_Admin_Reports_Coupons_Data_Store::sync_order_coupons( $order_id ),
WC_Admin_Reports_Taxes_Data_Store::sync_order_taxes( $order_id ),
)
);
// If all updates were either skipped or successful, we're done.
// The update methods return -1 for skip, or a boolean success indicator.
if ( 4 === absint( $result ) ) {
return;
}
// Otherwise assume an error occurred and reschedule.
self::schedule_single_order_process( $order_id );
}
/**
* Returns the batch size for regenerating reports.
* Note: can differ per batch action.
*
* @param string $action Single batch action name.
* @return int Batch size.
*/
public static function get_batch_size( $action ) {
$batch_sizes = array(
self::QUEUE_BATCH_ACTION => 100,
self::CUSTOMERS_BATCH_ACTION => 25,
self::ORDERS_BATCH_ACTION => 10,
);
$batch_size = isset( $batch_sizes[ $action ] ) ? $batch_sizes[ $action ] : 25;
/**
* Filter the batch size for regenerating a report table.
*
* @param int $batch_size Batch size.
* @param string $action Batch action name.
*/
return apply_filters( 'wc_admin_report_regenerate_batch_size', $batch_size, $action );
}
/**
* Queue a large number of batch jobs, respecting the batch size limit.
* Reduces a range of batches down to "single batch" jobs.
*
* @param int $range_start Starting batch number.
* @param int $range_end Ending batch number.
* @param string $single_batch_action Action to schedule for a single batch.
* @return void
*/
public static function queue_batches( $range_start, $range_end, $single_batch_action ) {
$batch_size = self::get_batch_size( self::QUEUE_BATCH_ACTION );
$range_size = 1 + ( $range_end - $range_start );
$action_timestamp = time() + 5;
if ( $range_size > $batch_size ) {
// If the current batch range is larger than a single batch,
// split the range into $queue_batch_size chunks.
$chunk_size = ceil( $range_size / $batch_size );
for ( $i = 0; $i < $batch_size; $i++ ) {
$batch_start = $range_start + ( $i * $chunk_size );
$batch_end = min( $range_end, $range_start + ( $chunk_size * ( $i + 1 ) ) - 1 );
self::queue()->schedule_single(
$action_timestamp,
self::QUEUE_BATCH_ACTION,
array( $batch_start, $batch_end, $single_batch_action )
);
}
} else {
// Otherwise, queue the single batches.
for ( $i = $range_start; $i <= $range_end; $i++ ) {
self::queue()->schedule_single( $action_timestamp, $single_batch_action, array( $i ) );
}
}
}
/**
* Queue an action to run after another.
*
* @param string $action Action to run after prerequisite.
* @param array $action_args Action arguments.
* @param string $prerequisite_action Prerequisite action.
*/
public static function queue_dependent_action( $action, $action_args, $prerequisite_action ) {
$blocking_jobs = self::queue()->search(
array(
'status' => 'pending',
'orderby' => 'date',
'order' => 'DESC',
'per_page' => 1,
'claimed' => false,
'search' => $prerequisite_action, // search is used instead of hook to find queued batch creation.
)
);
$next_job_schedule = null;
$blocking_job_hook = null;
if ( $blocking_jobs ) {
$blocking_job = current( $blocking_jobs );
$blocking_job_hook = $blocking_job->get_hook();
$next_job_schedule = $blocking_job->get_schedule()->next();
}
// Eliminate the false positive scenario where the blocking job is
// actually another queued dependent action awaiting the same prerequisite.
// Also, ensure that the next schedule is a DateTime (it can be null).
if (
is_a( $next_job_schedule, 'DateTime' ) &&
( self::QUEUE_DEPEDENT_ACTION !== $blocking_job_hook )
) {
self::queue()->schedule_single(
$next_job_schedule->getTimestamp() + 5,
self::QUEUE_DEPEDENT_ACTION,
array( $action, $action_args, $prerequisite_action )
);
} else {
self::queue()->schedule_single( time() + 5, $action, $action_args );
}
}
/**
* Init customer lookup table update (in batches).
*/
public static function customer_lookup_batch_init() {
$batch_size = self::get_batch_size( self::CUSTOMERS_BATCH_ACTION );
$customer_query = new WP_User_Query(
array(
'fields' => 'ID',
'number' => 1,
)
);
$total_customers = $customer_query->get_total();
if ( 0 === $total_customers ) {
return;
}
$num_batches = ceil( $total_customers / $batch_size );
self::queue_batches( 1, $num_batches, self::CUSTOMERS_BATCH_ACTION );
}
/**
* Process a batch of customers to update.
*
* @param int $batch_number Batch number to process (essentially a query page number).
* @return void
*/
public static function customer_lookup_process_batch( $batch_number ) {
$batch_size = self::get_batch_size( self::CUSTOMERS_BATCH_ACTION );
$customer_query = new WP_User_Query(
array(
'fields' => 'ID',
'orderby' => 'ID',
'order' => 'ASC',
'number' => $batch_size,
'paged' => $batch_number,
)
);
$customer_ids = $customer_query->get_results();
foreach ( $customer_ids as $customer_id ) {
// @todo Schedule single customer update if this fails?
WC_Admin_Reports_Customers_Data_Store::update_registered_customer( $customer_id );
}
}
/**
* Adds data stores.
*
@ -756,187 +370,27 @@ class WC_Admin_Api_Init {
return array_merge(
$data_stores,
array(
'report-revenue-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store',
'report-orders' => 'WC_Admin_Reports_Orders_Data_Store',
'report-orders-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store',
'report-products' => 'WC_Admin_Reports_Products_Data_Store',
'report-variations' => 'WC_Admin_Reports_Variations_Data_Store',
'report-products-stats' => 'WC_Admin_Reports_Products_Stats_Data_Store',
'report-categories' => 'WC_Admin_Reports_Categories_Data_Store',
'report-taxes' => 'WC_Admin_Reports_Taxes_Data_Store',
'report-taxes-stats' => 'WC_Admin_Reports_Taxes_Stats_Data_Store',
'report-coupons' => 'WC_Admin_Reports_Coupons_Data_Store',
'report-coupons-stats' => 'WC_Admin_Reports_Coupons_Stats_Data_Store',
'report-downloads' => 'WC_Admin_Reports_Downloads_Data_Store',
'report-revenue-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store',
'report-orders' => 'WC_Admin_Reports_Orders_Data_Store',
'report-orders-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store',
'report-products' => 'WC_Admin_Reports_Products_Data_Store',
'report-variations' => 'WC_Admin_Reports_Variations_Data_Store',
'report-products-stats' => 'WC_Admin_Reports_Products_Stats_Data_Store',
'report-categories' => 'WC_Admin_Reports_Categories_Data_Store',
'report-taxes' => 'WC_Admin_Reports_Taxes_Data_Store',
'report-taxes-stats' => 'WC_Admin_Reports_Taxes_Stats_Data_Store',
'report-coupons' => 'WC_Admin_Reports_Coupons_Data_Store',
'report-coupons-stats' => 'WC_Admin_Reports_Coupons_Stats_Data_Store',
'report-downloads' => 'WC_Admin_Reports_Downloads_Data_Store',
'report-downloads-stats' => 'WC_Admin_Reports_Downloads_Stats_Data_Store',
'admin-note' => 'WC_Admin_Notes_Data_Store',
'report-customers' => 'WC_Admin_Reports_Customers_Data_Store',
'report-customers-stats' => 'WC_Admin_Reports_Customers_Stats_Data_Store',
'report-stock-stats' => 'WC_Admin_Reports_Stock_Stats_Data_Store',
)
);
}
/**
* Adds new tables.
*
* @param array $wc_tables List of WooCommerce tables.
* @return array
*/
public static function add_tables( $wc_tables ) {
global $wpdb;
return array_merge(
$wc_tables,
array(
// @todo Will this work on multisite?
"{$wpdb->prefix}wc_order_stats",
"{$wpdb->prefix}wc_order_product_lookup",
"{$wpdb->prefix}wc_order_tax_lookup",
"{$wpdb->prefix}wc_order_coupon_lookup",
"{$wpdb->prefix}wc_admin_notes",
"{$wpdb->prefix}wc_admin_note_actions",
"{$wpdb->prefix}wc_customer_lookup",
)
);
}
/**
* Get database schema.
*
* @return string
*/
private static function get_schema() {
global $wpdb;
if ( $wpdb->has_cap( 'collation' ) ) {
$collate = $wpdb->get_charset_collate();
}
$tables = "
CREATE TABLE {$wpdb->prefix}wc_order_stats (
order_id bigint(20) unsigned NOT NULL,
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
num_items_sold int(11) UNSIGNED DEFAULT 0 NOT NULL,
gross_total double DEFAULT 0 NOT NULL,
coupon_total double DEFAULT 0 NOT NULL,
refund_total double DEFAULT 0 NOT NULL,
tax_total double DEFAULT 0 NOT NULL,
shipping_total double DEFAULT 0 NOT NULL,
net_total double DEFAULT 0 NOT NULL,
returning_customer boolean DEFAULT 0 NOT NULL,
status varchar(200) NOT NULL,
customer_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (order_id),
KEY date_created (date_created),
KEY customer_id (customer_id),
KEY status (status)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_product_lookup (
order_item_id BIGINT UNSIGNED NOT NULL,
order_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
variation_id BIGINT UNSIGNED NOT NULL,
customer_id BIGINT UNSIGNED NULL,
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
product_qty INT UNSIGNED NOT NULL,
product_net_revenue double DEFAULT 0 NOT NULL,
product_gross_revenue double DEFAULT 0 NOT NULL,
coupon_amount double DEFAULT 0 NOT NULL,
tax_amount double DEFAULT 0 NOT NULL,
shipping_amount double DEFAULT 0 NOT NULL,
shipping_tax_amount double DEFAULT 0 NOT NULL,
refund_amount double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_item_id),
KEY order_id (order_id),
KEY product_id (product_id),
KEY customer_id (customer_id),
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_tax_lookup (
order_id BIGINT UNSIGNED NOT NULL,
tax_rate_id BIGINT UNSIGNED NOT NULL,
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
shipping_tax double DEFAULT 0 NOT NULL,
order_tax double DEFAULT 0 NOT NULL,
total_tax double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_id, tax_rate_id),
KEY tax_rate_id (tax_rate_id),
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_coupon_lookup (
order_id BIGINT UNSIGNED NOT NULL,
coupon_id BIGINT UNSIGNED NOT NULL,
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
discount_amount double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_id, coupon_id),
KEY coupon_id (coupon_id),
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_admin_notes (
note_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
type varchar(20) NOT NULL,
locale varchar(20) NOT NULL,
title longtext NOT NULL,
content longtext NOT NULL,
icon varchar(200) NOT NULL,
content_data longtext NULL default null,
status varchar(200) NOT NULL,
source varchar(200) NOT NULL,
date_created datetime NOT NULL default '0000-00-00 00:00:00',
date_reminder datetime NULL default null,
PRIMARY KEY (note_id)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_admin_note_actions (
action_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
note_id BIGINT UNSIGNED NOT NULL,
name varchar(255) NOT NULL,
label varchar(255) NOT NULL,
query longtext NOT NULL,
PRIMARY KEY (action_id),
KEY note_id (note_id)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_customer_lookup (
customer_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED DEFAULT NULL,
username varchar(60) DEFAULT '' NOT NULL,
first_name varchar(255) NOT NULL,
last_name varchar(255) NOT NULL,
email varchar(100) NOT NULL,
date_last_active timestamp NULL default null,
date_registered timestamp NULL default null,
country char(2) DEFAULT '' NOT NULL,
postcode varchar(20) DEFAULT '' NOT NULL,
city varchar(100) DEFAULT '' NOT NULL,
PRIMARY KEY (customer_id),
UNIQUE KEY user_id (user_id),
KEY email (email)
) $collate;
";
return $tables;
}
/**
* Create database tables.
*/
public static function create_db_tables() {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( self::get_schema() );
}
/**
* Install plugin.
*/
public static function install() {
// Create tables.
self::create_db_tables();
// Initialize report tables.
add_action( 'woocommerce_after_register_post_type', array( __CLASS__, 'regenerate_report_data' ), 20 );
}
/**
* Add the currency symbol (in addition to currency code) to each Order
* object in REST API responses. For use in formatCurrency().

View File

@ -0,0 +1,264 @@
<?php
/**
* Installation related functions and actions.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Install Class.
*/
class WC_Admin_Install {
/**
* Plugin version.
*
* @TODO: get this dynamically?
*/
const VERSION_NUMBER = '0.6.0';
/**
* Plugin version option name.
*/
const VERSION_OPTION = 'wc_admin_version';
/**
* DB updates and callbacks that need to be run per version.
*
* @var array
*/
private static $db_updates = array();
/**
* Hook in tabs.
*/
public static function init() {
add_action( 'init', array( __CLASS__, 'check_version' ), 5 );
add_filter( 'wpmu_drop_tables', array( __CLASS__, 'wpmu_drop_tables' ) );
// Add wc-admin report tables to list of WooCommerce tables.
add_filter( 'woocommerce_install_get_tables', array( __CLASS__, 'add_tables' ) );
}
/**
* Check WC Admin version and run the updater is required.
*
* This check is done on all requests and runs if the versions do not match.
*/
public static function check_version() {
if (
! defined( 'IFRAME_REQUEST' ) &&
version_compare( get_option( self::VERSION_OPTION ), self::VERSION_NUMBER, '<' )
) {
self::install();
do_action( 'wc_admin_updated' );
}
}
/**
* Install WC Admin.
*/
public static function install() {
if ( ! is_blog_installed() ) {
return;
}
// Check if we are not already running this routine.
if ( 'yes' === get_transient( 'wc_admin_installing' ) ) {
return;
}
// If we made it till here nothing is running yet, lets set the transient now.
set_transient( 'wc_admin_installing', 'yes', MINUTE_IN_SECONDS * 10 );
wc_maybe_define_constant( 'WC_ADMIN_INSTALLING', true );
self::create_tables();
WC_Admin_Reports_Sync::regenerate_report_data();
self::update_wc_admin_version();
delete_transient( 'wc_admin_installing' );
do_action( 'wc_admin_installed' );
}
/**
* Get database schema.
*
* @return string
*/
private static function get_schema() {
global $wpdb;
if ( $wpdb->has_cap( 'collation' ) ) {
$collate = $wpdb->get_charset_collate();
}
$tables = "
CREATE TABLE {$wpdb->prefix}wc_order_stats (
order_id bigint(20) unsigned NOT NULL,
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
num_items_sold int(11) UNSIGNED DEFAULT 0 NOT NULL,
gross_total double DEFAULT 0 NOT NULL,
coupon_total double DEFAULT 0 NOT NULL,
refund_total double DEFAULT 0 NOT NULL,
tax_total double DEFAULT 0 NOT NULL,
shipping_total double DEFAULT 0 NOT NULL,
net_total double DEFAULT 0 NOT NULL,
returning_customer boolean DEFAULT 0 NOT NULL,
status varchar(200) NOT NULL,
customer_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (order_id),
KEY date_created (date_created),
KEY customer_id (customer_id),
KEY status (status)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_product_lookup (
order_item_id BIGINT UNSIGNED NOT NULL,
order_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
variation_id BIGINT UNSIGNED NOT NULL,
customer_id BIGINT UNSIGNED NULL,
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
product_qty INT UNSIGNED NOT NULL,
product_net_revenue double DEFAULT 0 NOT NULL,
product_gross_revenue double DEFAULT 0 NOT NULL,
coupon_amount double DEFAULT 0 NOT NULL,
tax_amount double DEFAULT 0 NOT NULL,
shipping_amount double DEFAULT 0 NOT NULL,
shipping_tax_amount double DEFAULT 0 NOT NULL,
refund_amount double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_item_id),
KEY order_id (order_id),
KEY product_id (product_id),
KEY customer_id (customer_id),
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_tax_lookup (
order_id BIGINT UNSIGNED NOT NULL,
tax_rate_id BIGINT UNSIGNED NOT NULL,
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
shipping_tax double DEFAULT 0 NOT NULL,
order_tax double DEFAULT 0 NOT NULL,
total_tax double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_id, tax_rate_id),
KEY tax_rate_id (tax_rate_id),
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_coupon_lookup (
order_id BIGINT UNSIGNED NOT NULL,
coupon_id BIGINT UNSIGNED NOT NULL,
date_created datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
discount_amount double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_id, coupon_id),
KEY coupon_id (coupon_id),
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_admin_notes (
note_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
type varchar(20) NOT NULL,
locale varchar(20) NOT NULL,
title longtext NOT NULL,
content longtext NOT NULL,
icon varchar(200) NOT NULL,
content_data longtext NULL default null,
status varchar(200) NOT NULL,
source varchar(200) NOT NULL,
date_created datetime NOT NULL default '0000-00-00 00:00:00',
date_reminder datetime NULL default null,
PRIMARY KEY (note_id)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_admin_note_actions (
action_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
note_id BIGINT UNSIGNED NOT NULL,
name varchar(255) NOT NULL,
label varchar(255) NOT NULL,
query longtext NOT NULL,
PRIMARY KEY (action_id),
KEY note_id (note_id)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_customer_lookup (
customer_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED DEFAULT NULL,
username varchar(60) DEFAULT '' NOT NULL,
first_name varchar(255) NOT NULL,
last_name varchar(255) NOT NULL,
email varchar(100) NULL default NULL,
date_last_active timestamp NULL default null,
date_registered timestamp NULL default null,
country char(2) DEFAULT '' NOT NULL,
postcode varchar(20) DEFAULT '' NOT NULL,
city varchar(100) DEFAULT '' NOT NULL,
PRIMARY KEY (customer_id),
UNIQUE KEY user_id (user_id),
KEY email (email)
) $collate;
";
return $tables;
}
/**
* Create database tables.
*/
public static function create_tables() {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( self::get_schema() );
}
/**
* Return a list of tables. Used to make sure all WC Admin tables are dropped
* when uninstalling the plugin in a single site or multi site environment.
*
* @return array WC tables.
*/
public static function get_tables() {
global $wpdb;
return array(
"{$wpdb->prefix}wc_order_stats",
"{$wpdb->prefix}wc_order_product_lookup",
"{$wpdb->prefix}wc_order_tax_lookup",
"{$wpdb->prefix}wc_order_coupon_lookup",
"{$wpdb->prefix}wc_admin_notes",
"{$wpdb->prefix}wc_admin_note_actions",
"{$wpdb->prefix}wc_customer_lookup",
);
}
/**
* Adds new tables.
*
* @param array $wc_tables List of WooCommerce tables.
* @return array
*/
public static function add_tables( $wc_tables ) {
return array_merge(
$wc_tables,
self::get_tables()
);
}
/**
* Uninstall tables when MU blog is deleted.
*
* @param array $tables List of tables that will be deleted by WP.
*
* @return string[]
*/
public static function wpmu_drop_tables( $tables ) {
return array_merge( $tables, self::get_tables() );
}
/**
* Update WC Admin version to current.
*/
private static function update_wc_admin_version() {
delete_option( self::VERSION_OPTION );
add_option( self::VERSION_OPTION, self::VERSION_NUMBER );
}
}
WC_Admin_Install::init();

View File

@ -26,6 +26,44 @@ class WC_Admin_Reports_Interval {
*/
public static $sql_datetime_format = 'Y-m-d H:i:s';
/**
* Converts local datetime to GMT/UTC time.
*
* @param string $datetime_string String representation of local datetime.
* @return DateTime
*/
public static function convert_local_datetime_to_gmt( $datetime_string ) {
$datetime = new DateTime( $datetime_string, new DateTimeZone( wc_timezone_string() ) );
$datetime->setTimezone( new DateTimeZone( 'GMT' ) );
return $datetime;
}
/**
* Returns default 'before' parameter for the reports.
*
* @return DateTime
*/
public static function default_before() {
$datetime = new DateTime();
$datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) );
return $datetime;
}
/**
* Returns default 'after' parameter for the reports.
*
* @return DateTime
*/
public static function default_after() {
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
$datetime = new DateTime();
$datetime->setTimestamp( $week_back );
$datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) );
return $datetime;
}
/**
* Returns date format to be used as grouping clause in SQL.
*
@ -62,7 +100,7 @@ class WC_Admin_Reports_Interval {
/**
* Returns quarter for the DateTime.
*
* @param DateTime $datetime Date & time.
* @param DateTime $datetime Local date & time.
* @return int|null
*/
public static function quarter( $datetime ) {
@ -94,7 +132,7 @@ class WC_Admin_Reports_Interval {
* The first week of the year is considered to be the week containing January 1.
* The second week starts on the next $first_day_of_week.
*
* @param DateTime $datetime Date for which the week number is to be calculated.
* @param DateTime $datetime Local date for which the week number is to be calculated.
* @param int $first_day_of_week 0 for Sunday to 6 for Saturday.
* @return int
*/
@ -112,7 +150,7 @@ class WC_Admin_Reports_Interval {
*
* @see WC_Admin_Reports_Interval::simple_week_number()
*
* @param DateTime $datetime Date for which the week number is to be calculated.
* @param DateTime $datetime Local date for which the week number is to be calculated.
* @param int $first_day_of_week 0 for Sunday to 6 for Saturday.
* @return int
*/
@ -159,22 +197,21 @@ class WC_Admin_Reports_Interval {
/**
* Calculates number of time intervals between two dates, closed interval on both sides.
*
* @param string $start Start date & time.
* @param string $end End date & time.
* @param string $interval Time interval increment, e.g. hour, day, week.
* @param DateTime $start_datetime Start date & time.
* @param DateTime $end_datetime End date & time.
* @param string $interval Time interval increment, e.g. hour, day, week.
*
* @return int
*/
public static function intervals_between( $start, $end, $interval ) {
$start_datetime = new DateTime( $start );
$end_datetime = new DateTime( $end );
public static function intervals_between( $start_datetime, $end_datetime, $interval ) {
switch ( $interval ) {
case 'hour':
$end_timestamp = (int) $end_datetime->format( 'U' );
$start_timestamp = (int) $start_datetime->format( 'U' );
$addendum = 0;
$end_min_sec = $end_timestamp % HOUR_IN_SECONDS;
$start_min_sec = $start_timestamp % HOUR_IN_SECONDS;
// modulo HOUR_IN_SECONDS would normally work, but there are non-full hour timezones, e.g. Nepal.
$start_min_sec = (int) $start_datetime->format( 'i' ) * MINUTE_IN_SECONDS + (int) $start_datetime->format( 's' );
$end_min_sec = (int) $end_datetime->format( 'i' ) * MINUTE_IN_SECONDS + (int) $end_datetime->format( 's' );
if ( $end_min_sec < $start_min_sec ) {
$addendum = 1;
}
@ -185,8 +222,8 @@ class WC_Admin_Reports_Interval {
$end_timestamp = (int) $end_datetime->format( 'U' );
$start_timestamp = (int) $start_datetime->format( 'U' );
$addendum = 0;
$end_hour_min_sec = $end_timestamp % DAY_IN_SECONDS;
$start_hour_min_sec = $start_timestamp % DAY_IN_SECONDS;
$end_hour_min_sec = (int) $end_datetime->format( 'H' ) * HOUR_IN_SECONDS + (int) $end_datetime->format( 'i' ) * MINUTE_IN_SECONDS + (int) $end_datetime->format( 's' );
$start_hour_min_sec = (int) $start_datetime->format( 'H' ) * HOUR_IN_SECONDS + (int) $start_datetime->format( 'i' ) * MINUTE_IN_SECONDS + (int) $start_datetime->format( 's' );
if ( $end_hour_min_sec < $start_hour_min_sec ) {
$addendum = 1;
}
@ -234,7 +271,8 @@ class WC_Admin_Reports_Interval {
public static function next_hour_start( $datetime, $reversed = false ) {
$hour_increment = $reversed ? 0 : 1;
$timestamp = (int) $datetime->format( 'U' );
$hours_offset_timestamp = ( floor( $timestamp / HOUR_IN_SECONDS ) + $hour_increment ) * HOUR_IN_SECONDS;
$seconds_into_hour = (int) $datetime->format( 'i' ) * MINUTE_IN_SECONDS + (int) $datetime->format( 's' );
$hours_offset_timestamp = $timestamp + ( $hour_increment * HOUR_IN_SECONDS - $seconds_into_hour );
if ( $reversed ) {
$hours_offset_timestamp --;
@ -242,6 +280,7 @@ class WC_Admin_Reports_Interval {
$hours_offset_time = new DateTime();
$hours_offset_time->setTimestamp( $hours_offset_timestamp );
$hours_offset_time->setTimezone( new DateTimeZone( wc_timezone_string() ) );
return $hours_offset_time;
}
@ -253,19 +292,19 @@ class WC_Admin_Reports_Interval {
* @return DateTime
*/
public static function next_day_start( $datetime, $reversed = false ) {
$day_increment = $reversed ? -1 : 1;
$day_increment = $reversed ? 0 : 1;
$timestamp = (int) $datetime->format( 'U' );
$next_day_timestamp = ( floor( $timestamp / DAY_IN_SECONDS ) + $day_increment ) * DAY_IN_SECONDS;
$next_day = new DateTime();
$next_day->setTimestamp( $next_day_timestamp );
$seconds_into_day = (int) $datetime->format( 'H' ) * HOUR_IN_SECONDS + (int) $datetime->format( 'i' ) * MINUTE_IN_SECONDS + (int) $datetime->format( 's' );
$next_day_timestamp = $timestamp + ( $day_increment * DAY_IN_SECONDS - $seconds_into_day );
// The day boundary is actually next midnight when going in reverse, so set it to day -1 at 23:59:59.
if ( $reversed ) {
$timestamp = (int) $next_day->format( 'U' );
$end_of_day_timestamp = floor( $timestamp / DAY_IN_SECONDS ) * DAY_IN_SECONDS + DAY_IN_SECONDS - 1;
$next_day->setTimestamp( $end_of_day_timestamp );
$next_day_timestamp --;
}
$next_day = new DateTime();
$next_day->setTimestamp( $next_day_timestamp );
$next_day->setTimezone( new DateTimeZone( wc_timezone_string() ) );
return $next_day;
}
@ -308,7 +347,7 @@ class WC_Admin_Reports_Interval {
$month = (int) $datetime->format( 'm' );
if ( $reversed ) {
$beg_of_month_datetime = new DateTime( "$year-$month-01 00:00:00" );
$beg_of_month_datetime = new DateTime( "$year-$month-01 00:00:00", new DateTimeZone( wc_timezone_string() ) );
$timestamp = (int) $beg_of_month_datetime->format( 'U' );
$end_of_prev_month_timestamp = $timestamp - 1;
$datetime->setTimestamp( $end_of_prev_month_timestamp );
@ -319,7 +358,7 @@ class WC_Admin_Reports_Interval {
$year ++;
}
$day = '01';
$datetime = new DateTime( "$year-$month-$day 00:00:00" );
$datetime = new DateTime( "$year-$month-$day 00:00:00", new DateTimeZone( wc_timezone_string() ) );
}
return $datetime;
@ -375,7 +414,7 @@ class WC_Admin_Reports_Interval {
}
break;
}
$datetime = new DateTime( "$year-$month-01 00:00:00" );
$datetime = new DateTime( "$year-$month-01 00:00:00", new DateTimeZone( wc_timezone_string() ) );
if ( $reversed ) {
$timestamp = (int) $datetime->format( 'U' );
$end_of_prev_month_timestamp = $timestamp - 1;
@ -399,13 +438,13 @@ class WC_Admin_Reports_Interval {
$day = '01';
if ( $reversed ) {
$datetime = new DateTime( "$year-$month-$day 00:00:00" );
$datetime = new DateTime( "$year-$month-$day 00:00:00", new DateTimeZone( wc_timezone_string() ) );
$timestamp = (int) $datetime->format( 'U' );
$end_of_prev_year_timestamp = $timestamp - 1;
$datetime->setTimestamp( $end_of_prev_year_timestamp );
} else {
$year += $year_increment;
$datetime = new DateTime( "$year-$month-$day 00:00:00" );
$datetime = new DateTime( "$year-$month-$day 00:00:00", new DateTimeZone( wc_timezone_string() ) );
}
return $datetime;
@ -422,9 +461,6 @@ class WC_Admin_Reports_Interval {
* @return DateTime
*/
public static function iterate( $datetime, $time_interval, $reversed = false ) {
// $result_datetime =
// $result_timestamp_adjusted = $result_datetime->format( 'U' ) - 1;
// $result_datetime->setTimestamp( $result_timestamp_adjusted );
return call_user_func( array( __CLASS__, "next_{$time_interval}_start" ), $datetime, $reversed );
}

View File

@ -94,8 +94,8 @@ class WC_Admin_Reports_Segmenting {
}
foreach ( $segments_db_result as $segment_data ) {
$segment_id = $segment_data[ $segment_dimension ];
$segment_labels = $this->get_segment_labels();
$segment_id = $segment_data[ $segment_dimension ];
$segment_labels = $this->get_segment_labels();
unset( $segment_data[ $segment_dimension ] );
$segment_datum = array(
'segment_id' => $segment_id,
@ -208,7 +208,7 @@ class WC_Admin_Reports_Segmenting {
*/
protected function merge_segment_intervals_results( $segment_dimension, $result1, $result2 ) {
$result_segments = array();
$segment_labels = $this->get_segment_labels();
$segment_labels = $this->get_segment_labels();
foreach ( $result1 as $segment_data ) {
$time_interval = $segment_data['time_interval'];
@ -317,8 +317,8 @@ class WC_Admin_Reports_Segmenting {
$segment_objects = wc_get_products( $args );
foreach ( $segment_objects as $segment ) {
$id = $segment->get_id();
$segments[] = $id;
$id = $segment->get_id();
$segments[] = $id;
$segment_labels[ $id ] = $segment->get_name();
}
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
@ -342,22 +342,33 @@ class WC_Admin_Reports_Segmenting {
$segment_objects = wc_get_products( $args );
foreach ( $segment_objects as $segment ) {
$id = $segment->get_id();
$segments[] = $id;
$id = $segment->get_id();
$segments[] = $id;
$segment_labels[ $id ] = $segment->get_name();
}
} elseif ( 'category' === $this->query_args['segmentby'] ) {
$categories = get_categories(
array(
'taxonomy' => 'product_cat',
)
$args = array(
'taxonomy' => 'product_cat',
);
$segments = wp_list_pluck( $categories, 'cat_ID' );
if ( isset( $this->query_args['categories'] ) ) {
$args['include'] = $this->query_args['categories'];
}
$categories = get_categories( $args );
$segments = wp_list_pluck( $categories, 'cat_ID' );
$segment_labels = wp_list_pluck( $categories, 'name', 'cat_ID' );
} elseif ( 'coupon' === $this->query_args['segmentby'] ) {
// @todo Switch to a non-direct-SQL way to get all coupons?
// @todo These are only currently existing coupons, but we should add also deleted ones, if they have been used at least once.
$coupon_ids = $wpdb->get_results( "SELECT ID FROM {$wpdb->prefix}posts WHERE post_type='shop_coupon' AND post_status='publish'", ARRAY_A ); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$segments = wp_list_pluck( $coupon_ids, 'ID' );
$args = array();
if ( isset( $this->query_args['coupons'] ) ) {
$args['include'] = $this->query_args['coupons'];
}
$coupons = WC_Admin_Reports_Coupons_Data_Store::get_coupons( $args );
$segments = wp_list_pluck( $coupons, 'ID' );
$segment_labels = wp_list_pluck( $coupons, 'post_title', 'ID' );
$segment_labels = array_map( 'wc_format_coupon_code', $segment_labels );
} elseif ( 'customer_type' === $this->query_args['segmentby'] ) {
// 0 -- new customer
// 1 -- returning customer

View File

@ -0,0 +1,29 @@
<?php
/**
* Class for stock stats report querying
*
* $report = new WC_Admin_Reports_Stock__Stats_Query();
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Reports_Stock_Stats_Query
*/
class WC_Admin_Reports_Stock_Stats_Query extends WC_Admin_Reports_Query {
/**
* Get product data based on the current query vars.
*
* @return array
*/
public function get_data() {
$data_store = WC_Data_Store::load( 'report-stock-stats' );
$results = $data_store->get_data();
return apply_filters( 'woocommerce_reports_stock_stats_query', $results );
}
}

View File

@ -0,0 +1,416 @@
<?php
/**
* Report table sync related functions and actions.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Reports_Sync Class.
*/
class WC_Admin_Reports_Sync {
/**
* Action hook for reducing a range of batches down to single actions.
*/
const QUEUE_BATCH_ACTION = 'wc-admin_queue_batches';
/**
* Action hook for queuing an action after another is complete.
*/
const QUEUE_DEPEDENT_ACTION = 'wc-admin_queue_dependent_action';
/**
* Action hook for processing a batch of customers.
*/
const CUSTOMERS_BATCH_ACTION = 'wc-admin_process_customers_batch';
/**
* Action hook for processing a batch of orders.
*/
const ORDERS_BATCH_ACTION = 'wc-admin_process_orders_batch';
/**
* Action hook for initializing the orders lookup batch creation.
*/
const ORDERS_LOOKUP_BATCH_INIT = 'wc-admin_orders_lookup_batch_init';
/**
* Action hook for processing a batch of orders.
*/
const SINGLE_ORDER_ACTION = 'wc-admin_process_order';
/**
* Queue instance.
*
* @var WC_Queue_Interface
*/
protected static $queue = null;
/**
* Get queue instance.
*
* @return WC_Queue_Interface
*/
public static function queue() {
if ( is_null( self::$queue ) ) {
self::$queue = WC()->queue();
}
return self::$queue;
}
/**
* Set queue instance.
*
* @param WC_Queue_Interface $queue Queue instance.
*/
public static function set_queue( $queue ) {
self::$queue = $queue;
}
/**
* Hook in sync methods.
*/
public static function init() {
// Add report regeneration to tools menu.
add_filter( 'woocommerce_debug_tools', array( __CLASS__, 'add_regenerate_tool' ) );
// Initialize syncing hooks.
add_action( 'wp_loaded', array( __CLASS__, 'orders_lookup_update_init' ) );
// Initialize scheduled action handlers.
add_action( self::QUEUE_BATCH_ACTION, array( __CLASS__, 'queue_batches' ), 10, 3 );
add_action( self::QUEUE_DEPEDENT_ACTION, array( __CLASS__, 'queue_dependent_action' ), 10, 3 );
add_action( self::CUSTOMERS_BATCH_ACTION, array( __CLASS__, 'customer_lookup_process_batch' ) );
add_action( self::ORDERS_BATCH_ACTION, array( __CLASS__, 'orders_lookup_process_batch' ) );
add_action( self::ORDERS_LOOKUP_BATCH_INIT, array( __CLASS__, 'orders_lookup_batch_init' ) );
add_action( self::SINGLE_ORDER_ACTION, array( __CLASS__, 'orders_lookup_process_order' ) );
}
/**
* Regenerate data for reports.
*/
public static function regenerate_report_data() {
// Add registered customers to the lookup table before updating order stats
// so that the orders can be associated with the `customer_id` column.
self::customer_lookup_batch_init();
// Queue orders lookup to occur after customers lookup generation is done.
self::queue_dependent_action( self::ORDERS_LOOKUP_BATCH_INIT, array(), self::CUSTOMERS_BATCH_ACTION );
}
/**
* Adds regenerate tool.
*
* @param array $tools List of tools.
* @return array
*/
public static function add_regenerate_tool( $tools ) {
return array_merge(
$tools,
array(
'rebuild_stats' => array(
'name' => __( 'Rebuild reports data', 'wc-admin' ),
'button' => __( 'Rebuild reports', 'wc-admin' ),
'desc' => __( 'This tool will rebuild all of the information used by the reports.', 'wc-admin' ),
'callback' => array( __CLASS__, 'regenerate_report_data' ),
),
)
);
}
/**
* Schedule an action to process a single Order.
*
* @param int $order_id Order ID.
* @return void
*/
public static function schedule_single_order_process( $order_id ) {
if ( 'shop_order' !== get_post_type( $order_id ) ) {
return;
}
if ( apply_filters( 'woocommerce_disable_order_scheduling', false ) ) {
self::orders_lookup_process_order( $order_id );
return;
}
// This can get called multiple times for a single order, so we look
// for existing pending jobs for the same order to avoid duplicating efforts.
$existing_jobs = self::queue()->search(
array(
'status' => 'pending',
'per_page' => 1,
'claimed' => false,
'search' => "[{$order_id}]",
)
);
if ( $existing_jobs ) {
$existing_job = current( $existing_jobs );
// Bail out if there's a pending single order action, or a pending dependent action.
if (
( self::SINGLE_ORDER_ACTION === $existing_job->get_hook() ) ||
(
self::QUEUE_DEPEDENT_ACTION === $existing_job->get_hook() &&
in_array( self::SINGLE_ORDER_ACTION, $existing_job->get_args() )
)
) {
return;
}
}
// We want to ensure that customer lookup updates are scheduled before order updates.
self::queue_dependent_action( self::SINGLE_ORDER_ACTION, array( $order_id ), self::CUSTOMERS_BATCH_ACTION );
}
/**
* Attach order lookup update hooks.
*/
public static function orders_lookup_update_init() {
// Activate WC_Order extension.
WC_Admin_Order::add_filters();
add_action( 'save_post', array( __CLASS__, 'schedule_single_order_process' ) );
add_action( 'woocommerce_order_refunded', array( __CLASS__, 'schedule_single_order_process' ) );
WC_Admin_Reports_Orders_Stats_Data_Store::init();
WC_Admin_Reports_Customers_Data_Store::init();
WC_Admin_Reports_Coupons_Data_Store::init();
WC_Admin_Reports_Products_Data_Store::init();
WC_Admin_Reports_Taxes_Data_Store::init();
}
/**
* Init order/product lookup tables update (in batches).
*/
public static function orders_lookup_batch_init() {
$batch_size = self::get_batch_size( self::ORDERS_BATCH_ACTION );
$order_query = new WC_Order_Query(
array(
'return' => 'ids',
'limit' => 1,
'paginate' => true,
)
);
$result = $order_query->get_orders();
if ( 0 === $result->total ) {
return;
}
$num_batches = ceil( $result->total / $batch_size );
self::queue_batches( 1, $num_batches, self::ORDERS_BATCH_ACTION );
}
/**
* Process a batch of orders to update (stats and products).
*
* @param int $batch_number Batch number to process (essentially a query page number).
* @return void
*/
public static function orders_lookup_process_batch( $batch_number ) {
$batch_size = self::get_batch_size( self::ORDERS_BATCH_ACTION );
$order_query = new WC_Order_Query(
array(
'return' => 'ids',
'limit' => $batch_size,
'page' => $batch_number,
'orderby' => 'ID',
'order' => 'ASC',
)
);
$order_ids = $order_query->get_orders();
foreach ( $order_ids as $order_id ) {
self::orders_lookup_process_order( $order_id );
}
}
/**
* Process a single order to update lookup tables for.
* If an error is encountered in one of the updates, a retry action is scheduled.
*
* @param int $order_id Order ID.
* @return void
*/
public static function orders_lookup_process_order( $order_id ) {
$result = array_sum(
array(
WC_Admin_Reports_Orders_Stats_Data_Store::sync_order( $order_id ),
WC_Admin_Reports_Products_Data_Store::sync_order_products( $order_id ),
WC_Admin_Reports_Coupons_Data_Store::sync_order_coupons( $order_id ),
WC_Admin_Reports_Taxes_Data_Store::sync_order_taxes( $order_id ),
)
);
// If all updates were either skipped or successful, we're done.
// The update methods return -1 for skip, or a boolean success indicator.
if ( 4 === absint( $result ) ) {
return;
}
// Otherwise assume an error occurred and reschedule.
self::schedule_single_order_process( $order_id );
}
/**
* Returns the batch size for regenerating reports.
* Note: can differ per batch action.
*
* @param string $action Single batch action name.
* @return int Batch size.
*/
public static function get_batch_size( $action ) {
$batch_sizes = array(
self::QUEUE_BATCH_ACTION => 100,
self::CUSTOMERS_BATCH_ACTION => 25,
self::ORDERS_BATCH_ACTION => 10,
);
$batch_size = isset( $batch_sizes[ $action ] ) ? $batch_sizes[ $action ] : 25;
/**
* Filter the batch size for regenerating a report table.
*
* @param int $batch_size Batch size.
* @param string $action Batch action name.
*/
return apply_filters( 'wc_admin_report_regenerate_batch_size', $batch_size, $action );
}
/**
* Queue a large number of batch jobs, respecting the batch size limit.
* Reduces a range of batches down to "single batch" jobs.
*
* @param int $range_start Starting batch number.
* @param int $range_end Ending batch number.
* @param string $single_batch_action Action to schedule for a single batch.
* @return void
*/
public static function queue_batches( $range_start, $range_end, $single_batch_action ) {
$batch_size = self::get_batch_size( self::QUEUE_BATCH_ACTION );
$range_size = 1 + ( $range_end - $range_start );
$action_timestamp = time() + 5;
if ( $range_size > $batch_size ) {
// If the current batch range is larger than a single batch,
// split the range into $queue_batch_size chunks.
$chunk_size = ceil( $range_size / $batch_size );
for ( $i = 0; $i < $batch_size; $i++ ) {
$batch_start = $range_start + ( $i * $chunk_size );
$batch_end = min( $range_end, $range_start + ( $chunk_size * ( $i + 1 ) ) - 1 );
self::queue()->schedule_single(
$action_timestamp,
self::QUEUE_BATCH_ACTION,
array( $batch_start, $batch_end, $single_batch_action )
);
}
} else {
// Otherwise, queue the single batches.
for ( $i = $range_start; $i <= $range_end; $i++ ) {
self::queue()->schedule_single( $action_timestamp, $single_batch_action, array( $i ) );
}
}
}
/**
* Queue an action to run after another.
*
* @param string $action Action to run after prerequisite.
* @param array $action_args Action arguments.
* @param string $prerequisite_action Prerequisite action.
*/
public static function queue_dependent_action( $action, $action_args, $prerequisite_action ) {
$blocking_jobs = self::queue()->search(
array(
'status' => 'pending',
'orderby' => 'date',
'order' => 'DESC',
'per_page' => 1,
'claimed' => false,
'search' => $prerequisite_action, // search is used instead of hook to find queued batch creation.
)
);
$next_job_schedule = null;
$blocking_job_hook = null;
if ( $blocking_jobs ) {
$blocking_job = current( $blocking_jobs );
$blocking_job_hook = $blocking_job->get_hook();
$next_job_schedule = $blocking_job->get_schedule()->next();
}
// Eliminate the false positive scenario where the blocking job is
// actually another queued dependent action awaiting the same prerequisite.
// Also, ensure that the next schedule is a DateTime (it can be null).
if (
is_a( $next_job_schedule, 'DateTime' ) &&
( self::QUEUE_DEPEDENT_ACTION !== $blocking_job_hook )
) {
self::queue()->schedule_single(
$next_job_schedule->getTimestamp() + 5,
self::QUEUE_DEPEDENT_ACTION,
array( $action, $action_args, $prerequisite_action )
);
} else {
self::queue()->schedule_single( time() + 5, $action, $action_args );
}
}
/**
* Init customer lookup table update (in batches).
*/
public static function customer_lookup_batch_init() {
$batch_size = self::get_batch_size( self::CUSTOMERS_BATCH_ACTION );
$customer_query = new WP_User_Query(
array(
'fields' => 'ID',
'number' => 1,
)
);
$total_customers = $customer_query->get_total();
if ( 0 === $total_customers ) {
return;
}
$num_batches = ceil( $total_customers / $batch_size );
self::queue_batches( 1, $num_batches, self::CUSTOMERS_BATCH_ACTION );
}
/**
* Process a batch of customers to update.
*
* @param int $batch_number Batch number to process (essentially a query page number).
* @return void
*/
public static function customer_lookup_process_batch( $batch_number ) {
$batch_size = self::get_batch_size( self::CUSTOMERS_BATCH_ACTION );
$customer_query = new WP_User_Query(
array(
'fields' => 'ID',
'orderby' => 'ID',
'order' => 'ASC',
'number' => $batch_size,
'paged' => $batch_number,
)
);
$customer_ids = $customer_query->get_results();
foreach ( $customer_ids as $customer_id ) {
// @todo Schedule single customer update if this fails?
WC_Admin_Reports_Customers_Data_Store::update_registered_customer( $customer_id );
}
}
}
WC_Admin_Reports_Sync::init();

View File

@ -212,22 +212,21 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'before' => WC_Admin_Reports_Interval::default_before(),
'after' => WC_Admin_Reports_Interval::default_after(),
'fields' => '*',
'categories' => array(),
'extended_info' => false,
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );

View File

@ -207,8 +207,6 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -216,13 +214,14 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
'page' => 1,
'order' => 'DESC',
'orderby' => 'coupon_id',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'before' => WC_Admin_Reports_Interval::default_before(),
'after' => WC_Admin_Reports_Interval::default_after(),
'fields' => '*',
'coupons' => array(),
'extended_info' => false,
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );
@ -336,7 +335,7 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
'order_id' => $order_id,
'coupon_id' => $coupon_id,
'discount_amount' => $coupon_item->get_discount(),
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
'date_created' => $order->get_date_created( 'edit' )->date( WC_Admin_Reports_Interval::$sql_datetime_format ),
),
array(
'%d',
@ -386,4 +385,22 @@ class WC_Admin_Reports_Coupons_Data_Store extends WC_Admin_Reports_Data_Store im
do_action( 'woocommerce_reports_delete_coupon', 0, $order_id );
}
/**
* Gets coupons based on the provided arguments.
*
* @todo Upon core merge, including this in core's `class-wc-coupon-data-store-cpt.php` might make more sense.
* @param array $args Array of args to filter the query by. Supports `include`.
* @return array Array of results.
*/
public static function get_coupons( $args ) {
global $wpdb;
$query = "SELECT ID, post_title FROM {$wpdb->prefix}posts WHERE post_type='shop_coupon'";
if ( ! empty( $args['include'] ) ) {
$included_coupons = implode( ',', $args['include'] );
$query .= " AND ID IN ({$included_coupons})";
}
return $wpdb->get_results( $query ); // WPCS: cache ok, DB call ok, unprepared SQL ok.
}
}

View File

@ -95,22 +95,21 @@ class WC_Admin_Reports_Coupons_Stats_Data_Store extends WC_Admin_Reports_Coupons
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// 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' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'fields' => '*',
'interval' => 'week',
'coupons' => array(),
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => WC_Admin_Reports_Interval::default_before(),
'after' => WC_Admin_Reports_Interval::default_after(),
'fields' => '*',
'interval' => 'week',
'coupons' => array(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );

View File

@ -25,7 +25,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
* @var array
*/
protected $column_types = array(
'customer_id' => 'intval',
'id' => 'intval',
'user_id' => 'intval',
'orders_count' => 'intval',
'total_spend' => 'floatval',
@ -38,7 +38,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
* @var array
*/
protected $report_columns = array(
'customer_id' => 'customer_id',
'id' => 'customer_id as id',
'user_id' => 'user_id',
'username' => 'username',
'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @todo What does this mean for RTL?
@ -60,7 +60,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
global $wpdb;
// Initialize some report columns that need disambiguation.
$this->report_columns['customer_id'] = $wpdb->prefix . self::TABLE_NAME . '.customer_id';
$this->report_columns['id'] = $wpdb->prefix . self::TABLE_NAME . '.customer_id as id';
$this->report_columns['date_last_order'] = "MAX( {$wpdb->prefix}wc_order_stats.date_created ) as date_last_order";
}
@ -230,8 +230,28 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
}
}
if ( ! empty( $query_args['name'] ) ) {
$where_clauses[] = $wpdb->prepare( "CONCAT_WS( ' ', first_name, last_name ) = %s", $query_args['name'] );
$search_params = array(
'name',
'username',
'email',
);
if ( ! empty( $query_args['search'] ) ) {
$name_like = '%' . $wpdb->esc_like( $query_args['search'] ) . '%';
if ( empty( $query_args['searchby'] ) || 'name' === $query_args['searchby'] || ! in_array( $query_args['searchby'], $search_params ) ) {
$searchby = "CONCAT_WS( ' ', first_name, last_name )";
} else {
$searchby = $query_args['searchby'];
}
$where_clauses[] = $wpdb->prepare( "{$searchby} LIKE %s", $name_like ); // WPCS: unprepared SQL ok.
}
// Allow a list of customer IDs to be specified.
if ( ! empty( $query_args['customers'] ) ) {
$included_customers = implode( ',', $query_args['customers'] );
$where_clauses[] = "{$customer_lookup_table}.customer_id IN ({$included_customers})";
}
$numeric_params = array(
@ -253,17 +273,18 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
$subclauses = array();
$min_param = $numeric_param . '_min';
$max_param = $numeric_param . '_max';
$or_equal = isset( $query_args[ $min_param ] ) && isset( $query_args[ $max_param ] ) ? '=' : '';
if ( isset( $query_args[ $min_param ] ) ) {
$subclauses[] = $wpdb->prepare(
"{$param_info['column']} >= {$param_info['format']}",
"{$param_info['column']} >{$or_equal} {$param_info['format']}",
$query_args[ $min_param ]
); // WPCS: unprepared SQL ok.
}
if ( isset( $query_args[ $max_param ] ) ) {
$subclauses[] = $wpdb->prepare(
"{$param_info['column']} <= {$param_info['format']}",
"{$param_info['column']} <{$or_equal} {$param_info['format']}",
$query_args[ $max_param ]
); // WPCS: unprepared SQL ok.
}
@ -312,6 +333,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
'fields' => '*',
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );
@ -392,52 +414,77 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
}
/**
* Gets the guest (no user_id) customer ID or creates a new one for
* the corresponding billing email in the provided WC_Order
* Returns an existing customer ID for an order if one exists.
*
* @param WC_Order $order Order to get/create guest customer data with.
* @return int|false The ID of the retrieved/created customer, or false on error.
* @param object $order WC Order.
* @return int|bool
*/
public function get_or_create_guest_customer_from_order( $order ) {
public static function get_existing_customer_id_from_order( $order ) {
$user_id = $order->get_customer_id();
if ( 0 === $user_id ) {
$email = $order->get_billing_email( 'edit' );
if ( $email ) {
return self::get_guest_id_by_email( $email );
} else {
return false;
}
} else {
return self::get_customer_id_by_user_id( $user_id );
}
}
/**
* Get or create a customer from a given order.
*
* @param object $order WC Order.
* @return int|bool
*/
public static function get_or_create_customer_from_order( $order ) {
global $wpdb;
$returning_customer_id = self::get_existing_customer_id_from_order( $order );
$email = $order->get_billing_email( 'edit' );
if ( empty( $email ) ) {
return false;
if ( $returning_customer_id ) {
return $returning_customer_id;
}
$existing_guest = $this->get_guest_by_email( $email );
if ( $existing_guest ) {
return $existing_guest['customer_id'];
}
$result = $wpdb->insert(
$wpdb->prefix . self::TABLE_NAME,
array(
'first_name' => $order->get_billing_first_name( 'edit' ),
'last_name' => $order->get_billing_last_name( 'edit' ),
'email' => $email,
'city' => $order->get_billing_city( 'edit' ),
'postcode' => $order->get_billing_postcode( 'edit' ),
'country' => $order->get_billing_country( 'edit' ),
'date_last_active' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
),
array(
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
)
$data = array(
'first_name' => $order->get_billing_first_name( 'edit' ),
'last_name' => $order->get_billing_last_name( 'edit' ),
'email' => $order->get_billing_email( 'edit' ),
'city' => $order->get_billing_city( 'edit' ),
'postcode' => $order->get_billing_postcode( 'edit' ),
'country' => $order->get_billing_country( 'edit' ),
'date_last_active' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
);
$format = array(
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
'%s',
);
// Add registered customer data.
if ( 0 !== $order->get_user_id() ) {
$user_id = $order->get_user_id();
$customer = new WC_Customer( $user_id );
$data['user_id'] = $user_id;
$data['username'] = $customer->get_username( 'edit' );
$data['date_registered'] = $customer->get_date_created( 'edit' )->date( WC_Admin_Reports_Interval::$sql_datetime_format );
$format[] = '%d';
$format[] = '%s';
$format[] = '%s';
}
$result = $wpdb->insert( $wpdb->prefix . self::TABLE_NAME, $data, $format );
$customer_id = $wpdb->insert_id;
/**
* Fires when customser's reports are created.
* Fires when a new report customer is created.
*
* @param int $customer_id Customer ID.
*/
@ -447,53 +494,23 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
}
/**
* Retrieve a guest (no user_id) customer row by email.
* Retrieve a guest ID (when user_id is null) by email.
*
* @param string $email Email address.
* @return false|array Customer array if found, boolean false if not.
*/
public function get_guest_by_email( $email ) {
public static function get_guest_id_by_email( $email ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$guest_row = $wpdb->get_row(
$table_name = $wpdb->prefix . self::TABLE_NAME;
$customer_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT * FROM {$table_name} WHERE email = %s AND user_id IS NULL LIMIT 1",
"SELECT customer_id FROM {$table_name} WHERE email = %s AND user_id IS NULL LIMIT 1",
$email
),
ARRAY_A
)
); // WPCS: unprepared SQL ok.
if ( $guest_row ) {
return $this->cast_numbers( $guest_row );
}
return false;
}
/**
* Retrieve a registered customer row by user_id.
*
* @param string|int $user_id User ID.
* @return false|array Customer array if found, boolean false if not.
*/
public function get_customer_by_user_id( $user_id ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$customer = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table_name} WHERE user_id = %d LIMIT 1",
$user_id
),
ARRAY_A
); // WPCS: unprepared SQL ok.
if ( $customer ) {
return $this->cast_numbers( $customer );
}
return false;
return $customer_id ? (int) $customer_id : false;
}
/**
@ -516,6 +533,30 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
return $customer_id ? (int) $customer_id : false;
}
/**
* Retrieve the oldest orders made by a customer.
*
* @param int $customer_id Customer ID.
* @return array Orders.
*/
public static function get_oldest_orders( $customer_id ) {
global $wpdb;
$orders_table = $wpdb->prefix . 'wc_order_stats';
$excluded_statuses = array_map( array( __CLASS__, 'normalize_order_status' ), self::get_excluded_report_order_statuses() );
$excluded_statuses_condition = '';
if ( ! empty( $excluded_statuses ) ) {
$excluded_statuses_str = implode( "','", $excluded_statuses );
$excluded_statuses_condition = "AND status NOT IN ('{$excluded_statuses_str}')";
}
return $wpdb->get_results(
$wpdb->prepare(
"SELECT order_id, date_created FROM {$orders_table} WHERE customer_id = %d {$excluded_statuses_condition} ORDER BY date_created, order_id ASC LIMIT 2",
$customer_id
)
); // WPCS: unprepared SQL ok.
}
/**
* Update the database with customer data.
*
@ -527,7 +568,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
$customer = new WC_Customer( $user_id );
if ( $customer->get_id() != $user_id ) {
if ( ! self::is_valid_customer( $user_id ) ) {
return false;
}
@ -541,7 +582,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
'city' => $customer->get_billing_city( 'edit' ),
'postcode' => $customer->get_billing_postcode( 'edit' ),
'country' => $customer->get_billing_country( 'edit' ),
'date_registered' => date( 'Y-m-d H:i:s', $customer->get_date_created( 'edit' )->getTimestamp() ),
'date_registered' => $customer->get_date_created( 'edit' )->date( WC_Admin_Reports_Interval::$sql_datetime_format ),
'date_last_active' => $last_active ? date( 'Y-m-d H:i:s', $last_active ) : null,
);
$format = array(
@ -576,6 +617,26 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
return $results;
}
/**
* Check if a user ID is a valid customer or other user role with past orders.
*
* @param int $user_id User ID.
* @return bool
*/
protected static function is_valid_customer( $user_id ) {
$customer = new WC_Customer( $user_id );
if ( $customer->get_id() !== $user_id ) {
return false;
}
if ( $customer->get_order_count() < 1 && 'customer' !== $customer->get_role() ) {
return false;
}
return true;
}
/**
* Returns string to be used as cache key for the data.
*

View File

@ -62,6 +62,8 @@ class WC_Admin_Reports_Customers_Stats_Data_Store extends WC_Admin_Reports_Custo
'fields' => '*',
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );

View File

@ -122,19 +122,18 @@ class WC_Admin_Reports_Data_Store {
* Fills in interval gaps from DB with 0-filled objects.
*
* @param array $db_intervals Array of all intervals present in the db.
* @param DateTime $datetime_start Start date.
* @param DateTime $datetime_end End date.
* @param DateTime $start_datetime Start date.
* @param DateTime $end_datetime End date.
* @param string $time_interval Time interval, e.g. day, week, month.
* @param stdClass $data Data with SQL extracted intervals.
* @return stdClass
*/
protected function fill_in_missing_intervals( $db_intervals, $datetime_start, $datetime_end, $time_interval, &$data ) {
protected function fill_in_missing_intervals( $db_intervals, $start_datetime, $end_datetime, $time_interval, &$data ) {
// @todo This is ugly and messy.
$local_tz = new DateTimeZone( wc_timezone_string() );
// At this point, we don't know when we can stop iterating, as the ordering can be based on any value.
$end_datetime = new DateTime( $datetime_end );
$time_ids = array_flip( wp_list_pluck( $data->intervals, 'time_interval' ) );
$db_intervals = array_flip( $db_intervals );
$datetime = new DateTime( $datetime_start );
// Totals object used to get all needed properties.
$totals_arr = get_object_vars( $data->totals );
foreach ( $totals_arr as $key => $val ) {
@ -142,9 +141,9 @@ class WC_Admin_Reports_Data_Store {
}
// @todo Should 'products' be in intervals?
unset( $totals_arr['products'] );
while ( $datetime <= $end_datetime ) {
$next_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval );
$time_id = WC_Admin_Reports_Interval::time_interval_id( $time_interval, $datetime );
while ( $start_datetime <= $end_datetime ) {
$next_start = WC_Admin_Reports_Interval::iterate( $start_datetime, $time_interval );
$time_id = WC_Admin_Reports_Interval::time_interval_id( $time_interval, $start_datetime );
// Either create fill-zero interval or use data from db.
if ( $next_start > $end_datetime ) {
$interval_end = $end_datetime->format( 'Y-m-d H:i:s' );
@ -152,27 +151,53 @@ class WC_Admin_Reports_Data_Store {
$prev_end_timestamp = (int) $next_start->format( 'U' ) - 1;
$prev_end = new DateTime();
$prev_end->setTimestamp( $prev_end_timestamp );
$prev_end->setTimezone( $local_tz );
$interval_end = $prev_end->format( 'Y-m-d H:i:s' );
}
if ( array_key_exists( $time_id, $time_ids ) ) {
// For interval present in the db for this time frame, just fill in dates.
$record = &$data->intervals[ $time_ids[ $time_id ] ];
$record['date_start'] = $datetime->format( 'Y-m-d H:i:s' );
$record['date_start'] = $start_datetime->format( 'Y-m-d H:i:s' );
$record['date_end'] = $interval_end;
} elseif ( ! array_key_exists( $time_id, $db_intervals ) ) {
// For intervals present in the db outside of this time frame, do nothing.
// For intervals not present in the db, fabricate it.
$record_arr = array();
$record_arr['time_interval'] = $time_id;
$record_arr['date_start'] = $datetime->format( 'Y-m-d H:i:s' );
$record_arr['date_start'] = $start_datetime->format( 'Y-m-d H:i:s' );
$record_arr['date_end'] = $interval_end;
$data->intervals[] = array_merge( $record_arr, $totals_arr );
}
$datetime = $next_start;
$start_datetime = $next_start;
}
return $data;
}
/**
* Converts input datetime parameters to local timezone. If there are no inputs from the user in query_args,
* uses default from $defaults.
*
* @param array $query_args Array of query arguments.
* @param array $defaults Array of default values.
*/
protected function normalize_timezones( &$query_args, $defaults ) {
$local_tz = new DateTimeZone( wc_timezone_string() );
foreach ( array( 'before', 'after' ) as $query_arg_key ) {
if ( isset( $query_args[ $query_arg_key ] ) && is_string( $query_args[ $query_arg_key ] ) ) {
// Assume that unspecified timezone is a local timezone.
$datetime = new DateTime( $query_args[ $query_arg_key ], $local_tz );
// In case timezone was forced by using +HH:MM, convert to local timezone.
$datetime->setTimezone( $local_tz );
$query_args[ $query_arg_key ] = $datetime;
} elseif ( isset( $query_args[ $query_arg_key ] ) && is_a( $query_args[ $query_arg_key ], 'DateTime' ) ) {
// In case timezone is in other timezone, convert to local timezone.
$query_args[ $query_arg_key ]->setTimezone( $local_tz );
} else {
$query_args[ $query_arg_key ] = isset( $defaults[ $query_arg_key ] ) ? $defaults[ $query_arg_key ] : null;
}
}
}
/**
* Removes extra records from intervals so that only requested number of records get returned.
*
@ -285,6 +310,7 @@ class WC_Admin_Reports_Data_Store {
if ( $db_interval_count === $expected_interval_count ) {
return;
}
$local_tz = new DateTimeZone( wc_timezone_string() );
if ( 'date' === strtolower( $query_args['orderby'] ) ) {
// page X in request translates to slightly different dates in the db, in case some
// records are missing from the db.
@ -292,9 +318,9 @@ class WC_Admin_Reports_Data_Store {
$end_iteration = 0;
if ( 'asc' === strtolower( $query_args['order'] ) ) {
// ORDER BY date ASC.
$new_start_date = new DateTime( $query_args['after'] );
$new_start_date = $query_args['after'];
$intervals_to_skip = ( $query_args['page'] - 1 ) * $intervals_query['per_page'];
$latest_end_date = new DateTime( $query_args['before'] );
$latest_end_date = $query_args['before'];
for ( $i = 0; $i < $intervals_to_skip; $i++ ) {
if ( $new_start_date > $latest_end_date ) {
$new_start_date = $latest_end_date;
@ -323,9 +349,9 @@ class WC_Admin_Reports_Data_Store {
}
} else {
// ORDER BY date DESC.
$new_end_date = new DateTime( $query_args['before'] );
$new_end_date = $query_args['before'];
$intervals_to_skip = ( $query_args['page'] - 1 ) * $intervals_query['per_page'];
$earliest_start_date = new DateTime( $query_args['after'] );
$earliest_start_date = $query_args['after'];
for ( $i = 0; $i < $intervals_to_skip; $i++ ) {
if ( $new_end_date < $earliest_start_date ) {
$new_end_date = $earliest_start_date;
@ -354,11 +380,13 @@ class WC_Admin_Reports_Data_Store {
$new_start_date->setTimestamp( $new_start_date_timestamp );
}
}
$query_args['adj_after'] = $new_start_date->format( WC_Admin_Reports_Interval::$iso_datetime_format );
$query_args['adj_before'] = $new_end_date->format( WC_Admin_Reports_Interval::$iso_datetime_format );
$query_args['adj_after'] = $new_start_date;
$query_args['adj_before'] = $new_end_date;
$adj_after = $new_start_date->format( WC_Admin_Reports_Interval::$sql_datetime_format );
$adj_before = $new_end_date->format( WC_Admin_Reports_Interval::$sql_datetime_format );
$intervals_query['where_time_clause'] = '';
$intervals_query['where_time_clause'] .= " AND {$table_name}.date_created <= '{$query_args['adj_before']}'";
$intervals_query['where_time_clause'] .= " AND {$table_name}.date_created >= '{$query_args['adj_after']}'";
$intervals_query['where_time_clause'] .= " AND {$table_name}.date_created <= '$adj_before'";
$intervals_query['where_time_clause'] .= " AND {$table_name}.date_created >= '$adj_after'";
$intervals_query['limit'] = 'LIMIT 0,' . $intervals_query['per_page'];
} else {
if ( 'asc' === $query_args['order'] ) {
@ -465,21 +493,21 @@ class WC_Admin_Reports_Data_Store {
*
* E.g. if there are db records for only Tuesday and Thursday this week, the actual week interval is [Mon, Sun], not [Tue, Thu].
*
* @param DateTime $datetime_start Start date.
* @param DateTime $datetime_end End date.
* @param DateTime $start_datetime Start date.
* @param DateTime $end_datetime End date.
* @param string $time_interval Time interval, e.g. day, week, month.
* @param array $intervals Array of intervals extracted from SQL db.
*/
protected function update_interval_boundary_dates( $datetime_start, $datetime_end, $time_interval, &$intervals ) {
protected function update_interval_boundary_dates( $start_datetime, $end_datetime, $time_interval, &$intervals ) {
$local_tz = new DateTimeZone( wc_timezone_string() );
foreach ( $intervals as $key => $interval ) {
$datetime = new DateTime( $interval['datetime_anchor'] );
$datetime = new DateTime( $interval['datetime_anchor'], $local_tz );
$prev_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval, true );
// @todo Not sure if the +1/-1 here are correct, especially as they are applied before the ?: below.
$prev_start_timestamp = (int) $prev_start->format( 'U' ) + 1;
$prev_start->setTimestamp( $prev_start_timestamp );
if ( $datetime_start ) {
$start_datetime = new DateTime( $datetime_start );
if ( $start_datetime ) {
$date_start = $prev_start < $start_datetime ? $start_datetime : $prev_start;
$intervals[ $key ]['date_start'] = $date_start->format( 'Y-m-d H:i:s' );
} else {
@ -489,8 +517,7 @@ class WC_Admin_Reports_Data_Store {
$next_end = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval );
$next_end_timestamp = (int) $next_end->format( 'U' ) - 1;
$next_end->setTimestamp( $next_end_timestamp );
if ( $datetime_end ) {
$end_datetime = new DateTime( $datetime_end );
if ( $end_datetime ) {
$date_end = $next_end > $end_datetime ? $end_datetime : $next_end;
$intervals[ $key ]['date_end'] = $date_end->format( 'Y-m-d H:i:s' );
} else {
@ -504,17 +531,21 @@ class WC_Admin_Reports_Data_Store {
/**
* Change structure of intervals to form a correct response.
*
* Also converts local datetimes to GMT and adds them to the intervals.
*
* @param array $intervals Time interval, e.g. day, week, month.
*/
protected function create_interval_subtotals( &$intervals ) {
foreach ( $intervals as $key => $interval ) {
$start_gmt = WC_Admin_Reports_Interval::convert_local_datetime_to_gmt( $interval['date_start'] );
$end_gmt = WC_Admin_Reports_Interval::convert_local_datetime_to_gmt( $interval['date_end'] );
// Move intervals result to subtotals object.
$intervals[ $key ] = array(
'interval' => $interval['time_interval'],
'date_start' => $interval['date_start'],
'date_start_gmt' => $interval['date_start'],
'date_start_gmt' => $start_gmt->format( WC_Admin_Reports_Interval::$sql_datetime_format ),
'date_end' => $interval['date_end'],
'date_end_gmt' => $interval['date_end'],
'date_end_gmt' => $end_gmt->format( WC_Admin_Reports_Interval::$sql_datetime_format ),
);
unset( $interval['interval'] );
@ -541,15 +572,13 @@ class WC_Admin_Reports_Data_Store {
);
if ( isset( $query_args['before'] ) && '' !== $query_args['before'] ) {
$datetime = new DateTime( $query_args['before'] );
$datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format );
$datetime_str = $query_args['before']->format( WC_Admin_Reports_Interval::$sql_datetime_format );
$sql_query['where_time_clause'] .= " AND {$table_name}.date_created <= '$datetime_str'";
}
if ( isset( $query_args['after'] ) && '' !== $query_args['after'] ) {
$datetime = new DateTime( $query_args['after'] );
$datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format );
$datetime_str = $query_args['after']->format( WC_Admin_Reports_Interval::$sql_datetime_format );
$sql_query['where_time_clause'] .= " AND {$table_name}.date_created >= '$datetime_str'";
}

View File

@ -122,27 +122,32 @@ class WC_Admin_Reports_Downloads_Data_Store extends WC_Admin_Reports_Data_Store
)";
}
$included_users = $this->get_included_users( $query_args );
$excluded_users = $this->get_excluded_users( $query_args );
if ( $included_users ) {
$customer_lookup_table = $wpdb->prefix . 'wc_customer_lookup';
$included_customers = $this->get_included_customers( $query_args );
$excluded_customers = $this->get_excluded_customers( $query_args );
if ( $included_customers ) {
$where_filters[] = " {$lookup_table}.permission_id IN (
SELECT
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
FROM
{$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE
{$wpdb->prefix}woocommerce_downloadable_product_permissions.user_id IN ({$included_users})
{$wpdb->prefix}woocommerce_downloadable_product_permissions.user_id IN (
SELECT {$customer_lookup_table}.user_id FROM {$customer_lookup_table} WHERE {$customer_lookup_table}.customer_id IN ({$included_customers})
)
)";
}
if ( $excluded_users ) {
if ( $excluded_customers ) {
$where_filters[] = " {$lookup_table}.permission_id NOT IN (
SELECT
DISTINCT {$wpdb->prefix}woocommerce_downloadable_product_permissions.permission_id
FROM
{$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE
{$wpdb->prefix}woocommerce_downloadable_product_permissions.user_id IN ({$excluded_users})
{$wpdb->prefix}woocommerce_downloadable_product_permissions.user_id IN (
SELECT {$customer_lookup_table}.user_id FROM {$customer_lookup_table} WHERE {$customer_lookup_table}.customer_id IN ({$excluded_customers})
)
)";
}
@ -205,6 +210,35 @@ class WC_Admin_Reports_Downloads_Data_Store extends WC_Admin_Reports_Data_Store
return $excluded_ips_str;
}
/**
* Returns comma separated ids of included customers, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
*/
protected function get_included_customers( $query_args ) {
$included_customers_str = '';
if ( isset( $query_args['customer_includes'] ) && is_array( $query_args['customer_includes'] ) && count( $query_args['customer_includes'] ) > 0 ) {
$included_customers_str = implode( ',', $query_args['customer_includes'] );
}
return $included_customers_str;
}
/**
* Returns comma separated ids of excluded customers, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return string
*/
protected function get_excluded_customers( $query_args ) {
$excluded_customer_str = '';
if ( isset( $query_args['customer_excludes'] ) && is_array( $query_args['customer_excludes'] ) && count( $query_args['customer_excludes'] ) > 0 ) {
$excluded_customer_str = implode( ',', $query_args['customer_excludes'] );
}
return $excluded_customer_str;
}
/**
* Fills WHERE clause of SQL request with date-related constraints.
@ -220,16 +254,14 @@ class WC_Admin_Reports_Downloads_Data_Store extends WC_Admin_Reports_Data_Store
'where_clause' => '',
);
if ( isset( $query_args['before'] ) && '' !== $query_args['before'] ) {
$datetime = new DateTime( $query_args['before'] );
$datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format );
if ( $query_args['before'] ) {
$datetime_str = $query_args['before']->format( WC_Admin_Reports_Interval::$sql_datetime_format );
$sql_query['where_time_clause'] .= " AND {$table_name}.timestamp <= '$datetime_str'";
}
if ( isset( $query_args['after'] ) && '' !== $query_args['after'] ) {
$datetime = new DateTime( $query_args['after'] );
$datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format );
if ( $query_args['after'] ) {
$datetime_str = $query_args['after']->format( WC_Admin_Reports_Interval::$sql_datetime_format );
$sql_query['where_time_clause'] .= " AND {$table_name}.timestamp >= '$datetime_str'";
}
@ -273,8 +305,6 @@ class WC_Admin_Reports_Downloads_Data_Store extends WC_Admin_Reports_Data_Store
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -282,11 +312,12 @@ class WC_Admin_Reports_Downloads_Data_Store extends WC_Admin_Reports_Data_Store
'page' => 1,
'order' => 'DESC',
'orderby' => 'timestamp',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'before' => WC_Admin_Reports_Interval::default_before(),
'after' => WC_Admin_Reports_Interval::default_after(),
'fields' => '*',
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );

View File

@ -48,8 +48,6 @@ class WC_Admin_Reports_Downloads_Stats_Data_Store extends WC_Admin_Reports_Downl
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -59,15 +57,11 @@ class WC_Admin_Reports_Downloads_Stats_Data_Store extends WC_Admin_Reports_Downl
'orderby' => 'date',
'fields' => '*',
'interval' => 'week',
'before' => WC_Admin_Reports_Interval::default_before(),
'after' => WC_Admin_Reports_Interval::default_after(),
);
$query_args = wp_parse_args( $query_args, $defaults );
if ( empty( $query_args['before'] ) ) {
$query_args['before'] = date( WC_Admin_Reports_Interval::$iso_datetime_format, $now );
}
if ( empty( $query_args['after'] ) ) {
$query_args['after'] = date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back );
}
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );
@ -190,43 +184,17 @@ class WC_Admin_Reports_Downloads_Stats_Data_Store extends WC_Admin_Reports_Downl
}
/**
* Sorts intervals according to user's request.
* Normalizes order_by clause to match to SQL query.
*
* They are pre-sorted in SQL, but after adding gaps, they need to be sorted including the added ones.
*
* @param stdClass $data Data object, must contain an array under $data->intervals.
* @param string $sort_by Ordering property.
* @param string $direction DESC/ASC.
*/
protected function sort_intervals( &$data, $sort_by, $direction ) {
if ( 'date' === $sort_by ) {
$this->order_by = 'time_interval';
} else {
$this->order_by = $sort_by;
}
$this->order = $direction;
usort( $data->intervals, array( $this, 'interval_cmp' ) );
}
/**
* Compares two report data objects by pre-defined object property and ASC/DESC ordering.
*
* @param stdClass $a Object a.
* @param stdClass $b Object b.
* @param string $order_by Order by option requeste by user.
* @return string
*/
protected function interval_cmp( $a, $b ) {
if ( '' === $this->order_by || '' === $this->order ) {
return 0;
}
if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) {
return 0;
} elseif ( $a[ $this->order_by ] > $b[ $this->order_by ] ) {
return strtolower( $this->order ) === 'desc' ? -1 : 1;
} elseif ( $a[ $this->order_by ] < $b[ $this->order_by ] ) {
return strtolower( $this->order ) === 'desc' ? 1 : -1;
protected function normalize_order_by( $order_by ) {
if ( 'date' === $order_by ) {
return 'time_interval';
}
return $order_by;
}
}

View File

@ -30,6 +30,8 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
'status' => 'strval',
'customer_id' => 'intval',
'net_total' => 'floatval',
'gross_total' => 'floatval',
'refund_total' => 'floatval',
'num_items_sold' => 'intval',
'customer_type' => 'strval',
);
@ -45,6 +47,8 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
'status' => 'REPLACE(status, "wc-", "") as status',
'customer_id' => 'customer_id',
'net_total' => 'net_total',
'gross_total' => 'gross_total',
'refund_total' => 'refund_total',
'num_items_sold' => 'num_items_sold',
'customer_type' => '(CASE WHEN returning_customer <> 0 THEN "returning" ELSE "new" END) as customer_type',
);
@ -124,8 +128,6 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -133,8 +135,8 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
'page' => 1,
'order' => 'DESC',
'orderby' => 'date_created',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'before' => WC_Admin_Reports_Interval::default_before(),
'after' => WC_Admin_Reports_Interval::default_after(),
'fields' => '*',
'product_includes' => array(),
'product_excludes' => array(),
@ -145,6 +147,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
'extended_info' => false,
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );
@ -228,6 +231,8 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
if ( 'date' === $order_by ) {
return 'date_created';
}
return $order_by;
}
/**
@ -242,6 +247,8 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
$mapped_products = $this->map_array_by_key( $products, 'product_id' );
$coupons = $this->get_coupons_by_order_ids( array_keys( $mapped_orders ) );
$product_categories = $this->get_product_categories_by_product_ids( array_keys( $mapped_products ) );
$customers = $this->get_customers_by_orders( $orders_data );
$mapped_customers = $this->map_array_by_key( $customers, 'customer_id' );
$mapped_data = array();
foreach ( $products as $product ) {
@ -279,8 +286,12 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
'products' => array(),
'categories' => array(),
'coupons' => array(),
'customer' => array(),
);
$orders_data[ $key ]['extended_info'] = isset( $mapped_data[ $order_data['order_id'] ] ) ? array_merge( $defaults, $mapped_data[ $order_data['order_id'] ] ) : $defaults;
if ( $order_data['customer_id'] && isset( $mapped_customers[ $order_data['customer_id'] ] ) ) {
$orders_data[ $key ]['extended_info']['customer'] = $mapped_customers[ $order_data['customer_id'] ];
}
}
}
@ -314,7 +325,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
"SELECT order_id, ID as product_id, post_title as product_name, product_qty as product_quantity
FROM {$wpdb->prefix}posts
JOIN {$order_product_lookup_table} ON {$order_product_lookup_table}.product_id = {$wpdb->prefix}posts.ID
WHERE
WHERE
order_id IN ({$included_order_ids})
",
ARRAY_A
@ -323,6 +334,32 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
return $products;
}
/**
* Get customer data from order IDs.
*
* @param array $orders Array of orders.
* @return array
*/
protected function get_customers_by_orders( $orders ) {
global $wpdb;
$customer_lookup_table = $wpdb->prefix . 'wc_customer_lookup';
$customer_ids = array();
foreach ( $orders as $order ) {
if ( $order['customer_id'] ) {
$customer_ids[] = $order['customer_id'];
}
}
$customer_ids = implode( ',', $customer_ids );
$customers = $wpdb->get_results(
"SELECT * FROM {$customer_lookup_table} WHERE customer_id IN ({$customer_ids})",
ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
return $customers;
}
/**
* Get coupon information from order IDs.
*
@ -338,7 +375,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp
"SELECT order_id, coupon_id, post_title as coupon_code
FROM {$wpdb->prefix}posts
JOIN {$order_coupon_lookup_table} ON {$order_coupon_lookup_table}.coupon_id = {$wpdb->prefix}posts.ID
WHERE
WHERE
order_id IN ({$included_order_ids})
",
ARRAY_A

View File

@ -180,8 +180,6 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only applied when not using REST API, as the API has its own defaults that overwrite these for most values (except before, after, etc).
$defaults = array(
@ -189,8 +187,8 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'before' => WC_Admin_Reports_Interval::default_before(),
'after' => WC_Admin_Reports_Interval::default_after(),
'interval' => 'week',
'fields' => '*',
'segmentby' => '',
@ -206,6 +204,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
'categories' => array(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );
@ -402,9 +401,10 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
'refund_total' => $order->get_total_refunded(),
'tax_total' => $order->get_total_tax(),
'shipping_total' => $order->get_shipping_total(),
'net_total' => (float) $order->get_total() - (float) $order->get_total_tax() - (float) $order->get_shipping_total(),
'net_total' => self::get_net_total( $order ),
'returning_customer' => self::is_returning_customer( $order ),
'status' => self::normalize_order_status( $order->get_status() ),
'customer_id' => WC_Admin_Reports_Customers_Data_Store::get_or_create_customer_from_order( $order ),
);
$format = array(
'%d',
@ -418,32 +418,9 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
'%f',
'%d',
'%s',
'%d',
);
// Ensure we're associating this order with a Customer in the lookup table.
$order_user_id = $order->get_customer_id();
$customers_data_store = new WC_Admin_Reports_Customers_Data_Store();
if ( 0 === $order_user_id ) {
$email = $order->get_billing_email( 'edit' );
if ( $email ) {
$customer_id = $customers_data_store->get_or_create_guest_customer_from_order( $order );
if ( $customer_id ) {
$data['customer_id'] = $customer_id;
$format[] = '%d';
}
}
} else {
$customer = $customers_data_store->get_customer_by_user_id( $order_user_id );
if ( $customer && $customer['customer_id'] ) {
$data['customer_id'] = $customer['customer_id'];
$format[] = '%d';
}
}
// Update or add the information to the DB.
$result = $wpdb->replace( $table_name, $data, $format );
@ -508,6 +485,23 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
return $num_items;
}
/**
* Get the net amount from an order without shipping, tax, or refunds.
*
* @param array $order WC_Order object.
* @return float
*/
protected static function get_net_total( $order ) {
$net_total = $order->get_total() - $order->get_total_tax() - $order->get_shipping_total();
$refunds = $order->get_refunds();
foreach ( $refunds as $refund ) {
$net_total += $refund->get_total() - $refund->get_total_tax() - $refund->get_shipping_total();
}
return $net_total > 0 ? (float) $net_total : 0;
}
/**
* Check to see if an order's customer has made previous orders or not
*
@ -515,24 +509,64 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
* @return bool
*/
protected static function is_returning_customer( $order ) {
global $wpdb;
$customer_id = WC_Admin_Reports_Customers_Data_Store::get_customer_id_by_user_id( $order->get_user_id() );
$orders_stats_table = $wpdb->prefix . self::TABLE_NAME;
$customer_id = WC_Admin_Reports_Customers_Data_Store::get_customer_id_by_user_id( $order->get_user_id() );
if ( ! $customer_id ) {
return false;
}
$customer_orders = $wpdb->get_var(
$oldest_orders = WC_Admin_Reports_Customers_Data_Store::get_oldest_orders( $customer_id );
if ( empty( $oldest_orders ) ) {
return false;
}
$first_order = $oldest_orders[0];
$second_order = isset( $oldest_orders[1] ) ? $oldest_orders[1] : false;
$excluded_statuses = self::get_excluded_report_order_statuses();
// Order is older than previous first order.
if ( $order->get_date_created() < new WC_DateTime( $first_order->date_created ) &&
! in_array( $order->get_status(), $excluded_statuses, true )
) {
self::set_customer_first_order( $customer_id, $order->get_id() );
return false;
}
// The current order is the oldest known order.
$is_first_order = (int) $order->get_id() === (int) $first_order->order_id;
// Order date has changed and next oldest is now the first order.
$date_change = $second_order &&
$order->get_date_created() > new WC_DateTime( $first_order->date_created ) &&
new WC_DateTime( $second_order->date_created ) < $order->get_date_created();
// Status has changed to an excluded status and next oldest order is now the first order.
$status_change = $second_order &&
in_array( $order->get_status(), $excluded_statuses, true );
if ( $is_first_order && ( $date_change || $status_change ) ) {
self::set_customer_first_order( $customer_id, $second_order->order_id );
return true;
}
return (int) $order->get_id() !== (int) $first_order->order_id;
}
/**
* Set a customer's first order and all others to returning.
*
* @param int $customer_id Customer ID.
* @param int $order_id Order ID.
*/
protected static function set_customer_first_order( $customer_id, $order_id ) {
global $wpdb;
$orders_stats_table = $wpdb->prefix . self::TABLE_NAME;
$wpdb->query(
$wpdb->prepare(
"SELECT COUNT(*) FROM ${orders_stats_table} WHERE customer_id = %d AND date_created < %s AND order_id != %d",
$customer_id,
date( 'Y-m-d H:i:s', $order->get_date_created()->getTimestamp() ),
$order->get_id()
"UPDATE ${orders_stats_table} SET returning_customer = CASE WHEN order_id = %d THEN false ELSE true END WHERE customer_id = %d",
$order_id,
$customer_id
)
);
return $customer_orders >= 1;
}
/**

View File

@ -229,8 +229,6 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -238,14 +236,15 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'before' => WC_Admin_Reports_Interval::default_before(),
'after' => WC_Admin_Reports_Interval::default_after(),
'fields' => '*',
'categories' => array(),
'product_includes' => array(),
'extended_info' => false,
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );
@ -402,7 +401,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i
'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null,
'product_qty' => $product_qty,
'product_net_revenue' => $net_revenue,
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
'date_created' => $order->get_date_created( 'edit' )->date( WC_Admin_Reports_Interval::$sql_datetime_format ),
'coupon_amount' => $coupon_amount,
'tax_amount' => $tax_amount,
'shipping_amount' => $shipping_amount,

View File

@ -103,8 +103,6 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -112,14 +110,15 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'before' => WC_Admin_Reports_Interval::default_before(),
'after' => WC_Admin_Reports_Interval::default_after(),
'fields' => '*',
'categories' => array(),
'interval' => 'week',
'product_includes' => array(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$product_data = wp_cache_get( $cache_key, $this->cache_group );

View File

@ -0,0 +1,138 @@
<?php
/**
* WC_Admin_Reports_Stock_Stats_Data_Store class file.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Reports_Stock_Stats_Data_Store.
*/
class WC_Admin_Reports_Stock_Stats_Data_Store extends WC_Admin_Reports_Data_Store implements WC_Admin_Reports_Data_Store_Interface {
/**
* Get stock counts for the whole store.
*
* @param array $query Not used for the stock stats data store, but needed for the interface.
* @return array Array of counts.
*/
public function get_data( $query ) {
$report_data = array();
$cache_expire = DAY_IN_SECONDS * 30;
$low_stock_transient_name = 'wc_admin_stock_count_lowstock';
$low_stock_count = get_transient( $low_stock_transient_name );
if ( false === $low_stock_count ) {
$low_stock_count = $this->get_low_stock_count();
set_transient( $low_stock_transient_name, $low_stock_count, $cache_expire );
}
$report_data['lowstock'] = $low_stock_count;
$status_options = wc_get_product_stock_status_options();
foreach ( $status_options as $status => $label ) {
$transient_name = 'wc_admin_stock_count_' . $status;
$count = get_transient( $transient_name );
if ( false === $count ) {
$count = $this->get_count( $status );
set_transient( $transient_name, $count, $cache_expire );
}
$report_data[ $status ] = $count;
}
$product_count_transient_name = 'wc_admin_product_count';
$product_count = get_transient( $product_count_transient_name );
if ( false === $product_count ) {
$product_count = $this->get_product_count();
set_transient( $product_count_transient_name, $product_count, $cache_expire );
}
$report_data['products'] = $product_count;
return $report_data;
}
/**
* Get low stock count.
*
* @return int Low stock count.
*/
private function get_low_stock_count() {
$query_args = array();
$query_args['post_type'] = array( 'product', 'product_variation' );
$low_stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
$no_stock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) );
$query_args['meta_query'] = array( // WPCS: slow query ok.
array(
'key' => '_manage_stock',
'value' => 'yes',
),
array(
'key' => '_stock',
'value' => array( $no_stock, $low_stock ),
'compare' => 'BETWEEN',
'type' => 'NUMERIC',
),
array(
'key' => '_stock_status',
'value' => 'instock',
),
);
$query = new WP_Query();
$query->query( $query_args );
return intval( $query->found_posts );
}
/**
* Get count for the passed in stock status.
*
* @param string $status Status slug.
* @return int Count.
*/
private function get_count( $status ) {
$query_args = array();
$query_args['post_type'] = array( 'product', 'product_variation' );
$query_args['meta_query'] = array( // WPCS: slow query ok.
array(
'key' => '_stock_status',
'value' => $status,
),
);
$query = new WP_Query();
$query->query( $query_args );
return intval( $query->found_posts );
}
/**
* Get product count for the store.
*
* @return int Product count.
*/
private function get_product_count() {
$query_args = array();
$query_args['post_type'] = array( 'product', 'product_variation' );
$query = new WP_Query();
$query->query( $query_args );
return intval( $query->found_posts );
}
}
/**
* Clear the count cache when products are added or updated, or when
* the no/low stock options are changed.
*
* @param int $id Post/product ID.
*/
function wc_admin_clear_stock_count_cache( $id ) {
delete_transient( 'wc_admin_stock_count_lowstock' );
delete_transient( 'wc_admin_product_count' );
$status_options = wc_get_product_stock_status_options();
foreach ( $status_options as $status => $label ) {
delete_transient( 'wc_admin_stock_count_' . $status );
}
}
add_action( 'woocommerce_update_product', 'wc_admin_clear_stock_count_cache' );
add_action( 'woocommerce_new_product', 'wc_admin_clear_stock_count_cache' );
add_action( 'update_option_woocommerce_notify_low_stock_amount', 'wc_admin_clear_stock_count_cache' );
add_action( 'update_option_woocommerce_notify_no_stock_amount', 'wc_admin_clear_stock_count_cache' );

View File

@ -71,6 +71,7 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
*/
public static function init() {
add_action( 'woocommerce_reports_delete_order_stats', array( __CLASS__, 'sync_on_order_delete' ), 15 );
add_action( 'woocommerce_refund_deleted', array( __CLASS__, 'sync_on_refund_delete' ), 10, 2 );
}
/**
@ -135,8 +136,6 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -144,12 +143,13 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
'page' => 1,
'order' => 'DESC',
'orderby' => 'tax_rate_id',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'before' => WC_Admin_Reports_Interval::default_before(),
'after' => WC_Admin_Reports_Interval::default_after(),
'fields' => '*',
'taxes' => array(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );
@ -268,15 +268,24 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
$num_updated = 0;
foreach ( $tax_items as $tax_item ) {
$item_refunds = 0;
$shipping_refunds = 0;
foreach ( $order->get_items() as $item_id => $item ) {
$item_refunds += $order->get_tax_refunded_for_item( $item_id, $tax_item->get_rate_id() );
}
foreach ( $order->get_items( 'shipping' ) as $item_id => $item ) {
$shipping_refunds += $order->get_tax_refunded_for_item( $item_id, $tax_item->get_rate_id(), 'shipping' );
}
$result = $wpdb->replace(
$wpdb->prefix . self::TABLE_NAME,
array(
'order_id' => $order->get_id(),
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
'date_created' => $order->get_date_created( 'edit' )->date( WC_Admin_Reports_Interval::$sql_datetime_format ),
'tax_rate_id' => $tax_item->get_rate_id(),
'shipping_tax' => $tax_item->get_shipping_tax_total(),
'order_tax' => $tax_item->get_tax_total(),
'total_tax' => $tax_item->get_tax_total() + $tax_item->get_shipping_tax_total(),
'shipping_tax' => $tax_item->get_shipping_tax_total() - $shipping_refunds,
'order_tax' => $tax_item->get_tax_total() - $item_refunds,
'total_tax' => $tax_item->get_tax_total() + $tax_item->get_shipping_tax_total() - $item_refunds - $shipping_refunds,
),
array(
'%d',
@ -327,4 +336,14 @@ class WC_Admin_Reports_Taxes_Data_Store extends WC_Admin_Reports_Data_Store impl
*/
do_action( 'woocommerce_reports_delete_tax', 0, $order_id );
}
/**
* Syncs tax information when a refund is deleted.
*
* @param int $refund_id Refund ID.
* @param int $order_id Order ID.
*/
public static function sync_on_refund_delete( $refund_id, $order_id ) {
self::sync_order_taxes( $order_id );
}
}

View File

@ -112,8 +112,6 @@ class WC_Admin_Reports_Taxes_Stats_Data_Store extends WC_Admin_Reports_Data_Stor
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
@ -121,12 +119,13 @@ class WC_Admin_Reports_Taxes_Stats_Data_Store extends WC_Admin_Reports_Data_Stor
'page' => 1,
'order' => 'DESC',
'orderby' => 'tax_rate_id',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'before' => WC_Admin_Reports_Interval::default_before(),
'after' => WC_Admin_Reports_Interval::default_after(),
'fields' => '*',
'taxes' => array(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );

View File

@ -188,23 +188,22 @@ class WC_Admin_Reports_Variations_Data_Store extends WC_Admin_Reports_Data_Store
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'before' => WC_Admin_Reports_Interval::default_before(),
'after' => WC_Admin_Reports_Interval::default_after(),
'fields' => '*',
'products' => array(),
'variations' => array(),
'extended_info' => false,
);
$query_args = wp_parse_args( $query_args, $defaults );
$this->normalize_timezones( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );

View File

@ -21,6 +21,10 @@ function wc_admin_is_admin_page() {
* `wc_get_screen_ids` will also return IDs for extensions that have properly registered themselves.
*/
function wc_admin_is_embed_enabled_wc_page() {
if ( ! wc_admin_is_feature_enabled( 'activity-panels' ) ) {
return false;
}
$screen_id = wc_admin_get_current_screen_id();
if ( ! $screen_id ) {
return false;
@ -66,106 +70,110 @@ function wc_admin_register_page( $options ) {
function wc_admin_register_pages() {
global $menu, $submenu;
add_submenu_page(
'woocommerce',
__( 'WooCommerce Dashboard', 'wc-admin' ),
__( 'Dashboard', 'wc-admin' ),
'manage_options',
'wc-admin',
'wc_admin_page'
);
if ( wc_admin_is_feature_enabled( 'dashboard' ) ) {
add_submenu_page(
'woocommerce',
__( 'WooCommerce Dashboard', 'wc-admin' ),
__( 'Dashboard', 'wc-admin' ),
'manage_options',
'wc-admin',
'wc_admin_page'
);
}
add_menu_page(
__( 'WooCommerce Analytics', 'wc-admin' ),
__( 'Analytics', 'wc-admin' ),
'manage_options',
'wc-admin#/analytics/revenue',
'wc_admin_page',
'dashicons-chart-bar',
56 // After WooCommerce & Product menu items.
);
if ( wc_admin_is_feature_enabled( 'analytics' ) ) {
add_menu_page(
__( 'WooCommerce Analytics', 'wc-admin' ),
__( 'Analytics', 'wc-admin' ),
'manage_options',
'wc-admin#/analytics/revenue',
'wc_admin_page',
'dashicons-chart-bar',
56 // After WooCommerce & Product menu items.
);
wc_admin_register_page(
array(
'title' => __( 'Revenue', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/revenue',
)
);
wc_admin_register_page(
array(
'title' => __( 'Revenue', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/revenue',
)
);
wc_admin_register_page(
array(
'title' => __( 'Orders', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/orders',
)
);
wc_admin_register_page(
array(
'title' => __( 'Orders', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/orders',
)
);
wc_admin_register_page(
array(
'title' => __( 'Products', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/products',
)
);
wc_admin_register_page(
array(
'title' => __( 'Products', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/products',
)
);
wc_admin_register_page(
array(
'title' => __( 'Categories', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/categories',
)
);
wc_admin_register_page(
array(
'title' => __( 'Categories', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/categories',
)
);
wc_admin_register_page(
array(
'title' => __( 'Coupons', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/coupons',
)
);
wc_admin_register_page(
array(
'title' => __( 'Coupons', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/coupons',
)
);
wc_admin_register_page(
array(
'title' => __( 'Taxes', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/taxes',
)
);
wc_admin_register_page(
array(
'title' => __( 'Taxes', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/taxes',
)
);
wc_admin_register_page(
array(
'title' => __( 'Downloads', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/downloads',
)
);
wc_admin_register_page(
array(
'title' => __( 'Downloads', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/downloads',
)
);
wc_admin_register_page(
array(
'title' => __( 'Stock', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/stock',
)
);
wc_admin_register_page(
array(
'title' => __( 'Stock', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/stock',
)
);
wc_admin_register_page(
array(
'title' => __( 'Customers', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/customers',
)
);
wc_admin_register_page(
array(
'title' => __( 'Customers', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/customers',
)
);
wc_admin_register_page(
array(
'title' => __( 'Settings', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/settings',
)
);
wc_admin_register_page(
array(
'title' => __( 'Settings', 'wc-admin' ),
'parent' => '/analytics/revenue',
'path' => '/analytics/settings',
)
);
}
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
if ( wc_admin_is_feature_enabled( 'devdocs' ) && defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
wc_admin_register_page(
array(
'title' => 'DevDocs',
@ -236,7 +244,7 @@ function wc_admin_enqueue_script() {
add_action( 'admin_enqueue_scripts', 'wc_admin_enqueue_script' );
/**
* Adds an admin body class.
* Adds body classes to the main wp-admin wrapper, allowing us to better target elements in specific scenarios.
*
* @param string $admin_body_class Body class to add.
*/
@ -252,6 +260,18 @@ function wc_admin_admin_body_class( $admin_body_class = '' ) {
if ( wc_admin_is_embed_enabled_wc_page() ) {
$classes[] = 'woocommerce-embed-page';
}
if ( function_exists( 'wc_admin_get_feature_config' ) ) {
$features = wc_admin_get_feature_config();
foreach ( $features as $feature_key => $bool ) {
if ( true === $bool ) {
$classes[] = sanitize_html_class( 'woocommerce-feature-enabled-' . $feature_key );
} else {
$classes[] = sanitize_html_class( 'woocommerce-feature-disabled-' . $feature_key );
}
}
}
$admin_body_class = implode( ' ', array_unique( $classes ) );
return " $admin_body_class ";
}
@ -261,7 +281,7 @@ add_filter( 'admin_body_class', 'wc_admin_admin_body_class' );
* Runs before admin notices action and hides them.
*/
function wc_admin_admin_before_notices() {
if ( ! wc_admin_is_admin_page() && ! wc_admin_is_embed_enabled_wc_page() ) {
if ( ( ! wc_admin_is_admin_page() && ! wc_admin_is_embed_enabled_wc_page() ) || ! wc_admin_is_feature_enabled( 'activity-panels' ) ) {
return;
}
echo '<div class="woocommerce-layout__notice-list-hide" id="wp__notice-list">';
@ -273,7 +293,7 @@ add_action( 'admin_notices', 'wc_admin_admin_before_notices', 0 );
* Runs after admin notices and closes div.
*/
function wc_admin_admin_after_notices() {
if ( ! wc_admin_is_admin_page() && ! wc_admin_is_embed_enabled_wc_page() ) {
if ( ( ! wc_admin_is_admin_page() && ! wc_admin_is_embed_enabled_wc_page() ) || ! wc_admin_is_feature_enabled( 'activity-panels' ) ) {
return;
}
echo '</div>';

View File

@ -280,3 +280,18 @@ function wc_admin_currency_settings() {
)
);
}
/**
* Returns if a specific wc-admin feature is enabled.
*
* @param string $feature Feature slug.
* @return bool Returns true if the feature is enabled.
* }
*/
function wc_admin_is_feature_enabled( $feature ) {
if ( ! function_exists( 'wc_admin_get_feature_config' ) ) {
return false;
}
$features = wc_admin_get_feature_config();
return isset( $features[ $feature ] ) && true === $features[ $feature ];
}

Some files were not shown because too many files have changed in this diff Show More