class-wc-api-reports.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. <?php
  2. /**
  3. * WooCommerce API Reports Class
  4. *
  5. * Handles requests to the /reports endpoint
  6. *
  7. * @author WooThemes
  8. * @category API
  9. * @package WooCommerce/API
  10. * @since 2.1
  11. * @version 2.1
  12. */
  13. if ( ! defined( 'ABSPATH' ) ) {
  14. exit; // Exit if accessed directly
  15. }
  16. class WC_API_Reports extends WC_API_Resource {
  17. /** @var string $base the route base */
  18. protected $base = '/reports';
  19. /** @var WC_Admin_Report instance */
  20. private $report;
  21. /**
  22. * Register the routes for this class
  23. *
  24. * GET /reports
  25. * GET /reports/sales
  26. *
  27. * @since 2.1
  28. * @param array $routes
  29. * @return array
  30. */
  31. public function register_routes( $routes ) {
  32. # GET /reports
  33. $routes[ $this->base ] = array(
  34. array( array( $this, 'get_reports' ), WC_API_Server::READABLE ),
  35. );
  36. # GET /reports/sales
  37. $routes[ $this->base . '/sales' ] = array(
  38. array( array( $this, 'get_sales_report' ), WC_API_Server::READABLE ),
  39. );
  40. # GET /reports/sales/top_sellers
  41. $routes[ $this->base . '/sales/top_sellers' ] = array(
  42. array( array( $this, 'get_top_sellers_report' ), WC_API_Server::READABLE ),
  43. );
  44. return $routes;
  45. }
  46. /**
  47. * Get a simple listing of available reports
  48. *
  49. * @since 2.1
  50. * @return array
  51. */
  52. public function get_reports() {
  53. return array( 'reports' => array( 'sales', 'sales/top_sellers' ) );
  54. }
  55. /**
  56. * Get the sales report
  57. *
  58. * @since 2.1
  59. * @param string $fields fields to include in response
  60. * @param array $filter date filtering
  61. * @return array|WP_Error
  62. */
  63. public function get_sales_report( $fields = null, $filter = array() ) {
  64. // check user permissions
  65. $check = $this->validate_request();
  66. if ( is_wp_error( $check ) ) {
  67. return $check;
  68. }
  69. // set date filtering
  70. $this->setup_report( $filter );
  71. // total sales, taxes, shipping, and order count
  72. $totals = $this->report->get_order_report_data( array(
  73. 'data' => array(
  74. '_order_total' => array(
  75. 'type' => 'meta',
  76. 'function' => 'SUM',
  77. 'name' => 'sales',
  78. ),
  79. '_order_tax' => array(
  80. 'type' => 'meta',
  81. 'function' => 'SUM',
  82. 'name' => 'tax',
  83. ),
  84. '_order_shipping_tax' => array(
  85. 'type' => 'meta',
  86. 'function' => 'SUM',
  87. 'name' => 'shipping_tax',
  88. ),
  89. '_order_shipping' => array(
  90. 'type' => 'meta',
  91. 'function' => 'SUM',
  92. 'name' => 'shipping',
  93. ),
  94. 'ID' => array(
  95. 'type' => 'post_data',
  96. 'function' => 'COUNT',
  97. 'name' => 'order_count',
  98. ),
  99. ),
  100. 'filter_range' => true,
  101. ) );
  102. // total items ordered
  103. $total_items = absint( $this->report->get_order_report_data( array(
  104. 'data' => array(
  105. '_qty' => array(
  106. 'type' => 'order_item_meta',
  107. 'order_item_type' => 'line_item',
  108. 'function' => 'SUM',
  109. 'name' => 'order_item_qty',
  110. ),
  111. ),
  112. 'query_type' => 'get_var',
  113. 'filter_range' => true,
  114. ) ) );
  115. // total discount used
  116. $total_discount = $this->report->get_order_report_data( array(
  117. 'data' => array(
  118. 'discount_amount' => array(
  119. 'type' => 'order_item_meta',
  120. 'order_item_type' => 'coupon',
  121. 'function' => 'SUM',
  122. 'name' => 'discount_amount',
  123. ),
  124. ),
  125. 'where' => array(
  126. array(
  127. 'key' => 'order_item_type',
  128. 'value' => 'coupon',
  129. 'operator' => '=',
  130. ),
  131. ),
  132. 'query_type' => 'get_var',
  133. 'filter_range' => true,
  134. ) );
  135. // new customers
  136. $users_query = new WP_User_Query(
  137. array(
  138. 'fields' => array( 'user_registered' ),
  139. 'role' => 'customer',
  140. )
  141. );
  142. $customers = $users_query->get_results();
  143. foreach ( $customers as $key => $customer ) {
  144. if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
  145. unset( $customers[ $key ] );
  146. }
  147. }
  148. $total_customers = count( $customers );
  149. // get order totals grouped by period
  150. $orders = $this->report->get_order_report_data( array(
  151. 'data' => array(
  152. '_order_total' => array(
  153. 'type' => 'meta',
  154. 'function' => 'SUM',
  155. 'name' => 'total_sales',
  156. ),
  157. '_order_shipping' => array(
  158. 'type' => 'meta',
  159. 'function' => 'SUM',
  160. 'name' => 'total_shipping',
  161. ),
  162. '_order_tax' => array(
  163. 'type' => 'meta',
  164. 'function' => 'SUM',
  165. 'name' => 'total_tax',
  166. ),
  167. '_order_shipping_tax' => array(
  168. 'type' => 'meta',
  169. 'function' => 'SUM',
  170. 'name' => 'total_shipping_tax',
  171. ),
  172. 'ID' => array(
  173. 'type' => 'post_data',
  174. 'function' => 'COUNT',
  175. 'name' => 'total_orders',
  176. 'distinct' => true,
  177. ),
  178. 'post_date' => array(
  179. 'type' => 'post_data',
  180. 'function' => '',
  181. 'name' => 'post_date',
  182. ),
  183. ),
  184. 'group_by' => $this->report->group_by_query,
  185. 'order_by' => 'post_date ASC',
  186. 'query_type' => 'get_results',
  187. 'filter_range' => true,
  188. ) );
  189. // get order item totals grouped by period
  190. $order_items = $this->report->get_order_report_data( array(
  191. 'data' => array(
  192. '_qty' => array(
  193. 'type' => 'order_item_meta',
  194. 'order_item_type' => 'line_item',
  195. 'function' => 'SUM',
  196. 'name' => 'order_item_count',
  197. ),
  198. 'post_date' => array(
  199. 'type' => 'post_data',
  200. 'function' => '',
  201. 'name' => 'post_date',
  202. ),
  203. ),
  204. 'where' => array(
  205. array(
  206. 'key' => 'order_item_type',
  207. 'value' => 'line_item',
  208. 'operator' => '=',
  209. ),
  210. ),
  211. 'group_by' => $this->report->group_by_query,
  212. 'order_by' => 'post_date ASC',
  213. 'query_type' => 'get_results',
  214. 'filter_range' => true,
  215. ) );
  216. // get discount totals grouped by period
  217. $discounts = $this->report->get_order_report_data( array(
  218. 'data' => array(
  219. 'discount_amount' => array(
  220. 'type' => 'order_item_meta',
  221. 'order_item_type' => 'coupon',
  222. 'function' => 'SUM',
  223. 'name' => 'discount_amount',
  224. ),
  225. 'post_date' => array(
  226. 'type' => 'post_data',
  227. 'function' => '',
  228. 'name' => 'post_date',
  229. ),
  230. ),
  231. 'where' => array(
  232. array(
  233. 'key' => 'order_item_type',
  234. 'value' => 'coupon',
  235. 'operator' => '=',
  236. ),
  237. ),
  238. 'group_by' => $this->report->group_by_query . ', order_item_name',
  239. 'order_by' => 'post_date ASC',
  240. 'query_type' => 'get_results',
  241. 'filter_range' => true,
  242. ) );
  243. $period_totals = array();
  244. // setup period totals by ensuring each period in the interval has data
  245. for ( $i = 0; $i <= $this->report->chart_interval; $i ++ ) {
  246. switch ( $this->report->chart_groupby ) {
  247. case 'day' :
  248. $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
  249. break;
  250. case 'month' :
  251. $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
  252. break;
  253. }
  254. // set the customer signups for each period
  255. $customer_count = 0;
  256. foreach ( $customers as $customer ) {
  257. if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
  258. $customer_count++;
  259. }
  260. }
  261. $period_totals[ $time ] = array(
  262. 'sales' => wc_format_decimal( 0.00, 2 ),
  263. 'orders' => 0,
  264. 'items' => 0,
  265. 'tax' => wc_format_decimal( 0.00, 2 ),
  266. 'shipping' => wc_format_decimal( 0.00, 2 ),
  267. 'discount' => wc_format_decimal( 0.00, 2 ),
  268. 'customers' => $customer_count,
  269. );
  270. }
  271. // add total sales, total order count, total tax and total shipping for each period
  272. foreach ( $orders as $order ) {
  273. $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
  274. if ( ! isset( $period_totals[ $time ] ) ) {
  275. continue;
  276. }
  277. $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 );
  278. $period_totals[ $time ]['orders'] = (int) $order->total_orders;
  279. $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
  280. $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
  281. }
  282. // add total order items for each period
  283. foreach ( $order_items as $order_item ) {
  284. $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );
  285. if ( ! isset( $period_totals[ $time ] ) ) {
  286. continue;
  287. }
  288. $period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
  289. }
  290. // add total discount for each period
  291. foreach ( $discounts as $discount ) {
  292. $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );
  293. if ( ! isset( $period_totals[ $time ] ) ) {
  294. continue;
  295. }
  296. $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
  297. }
  298. $sales_data = array(
  299. 'total_sales' => wc_format_decimal( $totals->sales, 2 ),
  300. 'average_sales' => wc_format_decimal( $totals->sales / ( $this->report->chart_interval + 1 ), 2 ),
  301. 'total_orders' => (int) $totals->order_count,
  302. 'total_items' => $total_items,
  303. 'total_tax' => wc_format_decimal( $totals->tax + $totals->shipping_tax, 2 ),
  304. 'total_shipping' => wc_format_decimal( $totals->shipping, 2 ),
  305. 'total_discount' => is_null( $total_discount ) ? wc_format_decimal( 0.00, 2 ) : wc_format_decimal( $total_discount, 2 ),
  306. 'totals_grouped_by' => $this->report->chart_groupby,
  307. 'totals' => $period_totals,
  308. 'total_customers' => $total_customers,
  309. );
  310. return array( 'sales' => apply_filters( 'woocommerce_api_report_response', $sales_data, $this->report, $fields, $this->server ) );
  311. }
  312. /**
  313. * Get the top sellers report
  314. *
  315. * @since 2.1
  316. * @param string $fields fields to include in response
  317. * @param array $filter date filtering
  318. * @return array|WP_Error
  319. */
  320. public function get_top_sellers_report( $fields = null, $filter = array() ) {
  321. // check user permissions
  322. $check = $this->validate_request();
  323. if ( is_wp_error( $check ) ) {
  324. return $check;
  325. }
  326. // set date filtering
  327. $this->setup_report( $filter );
  328. $top_sellers = $this->report->get_order_report_data( array(
  329. 'data' => array(
  330. '_product_id' => array(
  331. 'type' => 'order_item_meta',
  332. 'order_item_type' => 'line_item',
  333. 'function' => '',
  334. 'name' => 'product_id',
  335. ),
  336. '_qty' => array(
  337. 'type' => 'order_item_meta',
  338. 'order_item_type' => 'line_item',
  339. 'function' => 'SUM',
  340. 'name' => 'order_item_qty',
  341. ),
  342. ),
  343. 'order_by' => 'order_item_qty DESC',
  344. 'group_by' => 'product_id',
  345. 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
  346. 'query_type' => 'get_results',
  347. 'filter_range' => true,
  348. ) );
  349. $top_sellers_data = array();
  350. foreach ( $top_sellers as $top_seller ) {
  351. $product = wc_get_product( $top_seller->product_id );
  352. $top_sellers_data[] = array(
  353. 'title' => $product->get_name(),
  354. 'product_id' => $top_seller->product_id,
  355. 'quantity' => $top_seller->order_item_qty,
  356. );
  357. }
  358. return array( 'top_sellers' => apply_filters( 'woocommerce_api_report_response', $top_sellers_data, $this->report, $fields, $this->server ) );
  359. }
  360. /**
  361. * Setup the report object and parse any date filtering
  362. *
  363. * @since 2.1
  364. * @param array $filter date filtering
  365. */
  366. private function setup_report( $filter ) {
  367. include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
  368. $this->report = new WC_Admin_Report();
  369. if ( empty( $filter['period'] ) ) {
  370. // custom date range
  371. $filter['period'] = 'custom';
  372. if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {
  373. // overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges
  374. $_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] );
  375. $_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null;
  376. } else {
  377. // default custom range to today
  378. $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
  379. }
  380. } else {
  381. // ensure period is valid
  382. if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) {
  383. $filter['period'] = 'week';
  384. }
  385. // TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods
  386. // allow "week" for period instead of "7day"
  387. if ( 'week' === $filter['period'] ) {
  388. $filter['period'] = '7day';
  389. }
  390. }
  391. $this->report->calculate_current_range( $filter['period'] );
  392. }
  393. /**
  394. * Verify that the current user has permission to view reports
  395. *
  396. * @since 2.1
  397. * @see WC_API_Resource::validate_request()
  398. * @param null $id unused
  399. * @param null $type unused
  400. * @param null $context unused
  401. * @return true|WP_Error
  402. */
  403. protected function validate_request( $id = null, $type = null, $context = null ) {
  404. if ( ! current_user_can( 'view_woocommerce_reports' ) ) {
  405. return new WP_Error( 'woocommerce_api_user_cannot_read_report', __( 'You do not have permission to read this report', 'woocommerce' ), array( 'status' => 401 ) );
  406. } else {
  407. return true;
  408. }
  409. }
  410. }