From 87ff36db123136b1ada111798e4a8e7781111551 Mon Sep 17 00:00:00 2001 From: Max Rice Date: Mon, 18 Nov 2013 16:47:38 -0500 Subject: [PATCH] Properly handle datetimes Part of #4055 --- includes/api/class-wc-api-coupons.php | 13 ++- includes/api/class-wc-api-customers.php | 23 ++-- includes/api/class-wc-api-json-handler.php | 2 +- includes/api/class-wc-api-orders.php | 51 +++++---- includes/api/class-wc-api-products.php | 61 +++++------ includes/api/class-wc-api-reports.php | 69 +++++++----- includes/api/class-wc-api-resource.php | 12 +-- includes/api/class-wc-api-server.php | 117 +++++++++++---------- includes/wc-formatting-functions.php | 47 ++++++++- 9 files changed, 236 insertions(+), 159 deletions(-) diff --git a/includes/api/class-wc-api-coupons.php b/includes/api/class-wc-api-coupons.php index 1e1dff93cd4..b5e6d5ad8ba 100644 --- a/includes/api/class-wc-api-coupons.php +++ b/includes/api/class-wc-api-coupons.php @@ -49,8 +49,8 @@ class WC_API_Coupons extends WC_API_Resource { array( array( $this, 'delete_coupon' ), WC_API_Server::DELETABLE ), ); - # GET /coupons/code/ - $routes[ $this->base . '/code/(?P\w[\w\s\-]*)' ] = array( // note that coupon codes can contain spaces, dashes and underscores + # GET /coupons/code/, note that coupon codes can contain spaces, dashes and underscores + $routes[ $this->base . '/code/(?P\w[\w\s\-]*)' ] = array( array( array( $this, 'get_coupon_by_code' ), WC_API_Server::READABLE ), ); @@ -108,11 +108,15 @@ class WC_API_Coupons extends WC_API_Resource { $coupon = new WC_Coupon( $code ); + $coupon_post = get_post( $coupon->id ); + $coupon_data = array( 'id' => $coupon->id, 'code' => $coupon->code, 'type' => $coupon->type, - 'amount' => (string) number_format( $coupon->amount, 2 ), + 'created_at' => $this->server->format_datetime( $coupon_post->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $coupon_post->post_modified_gmt ), + 'amount' => woocommerce_format_decimal( $coupon->amount ), 'individual_use' => $coupon->individual_use, 'product_ids' => $coupon->product_ids, 'exclude_product_ids' => $coupon->exclude_product_ids, @@ -120,7 +124,7 @@ class WC_API_Coupons extends WC_API_Resource { 'usage_limit_per_user' => $coupon->usage_limit_per_user, 'limit_usage_to_x_items' => $coupon->limit_usage_to_x_items, 'usage_count' => $coupon->usage_count, - 'expiry_date' => $coupon->expiry_date, + 'expiry_date' => $this->server->format_datetime( $coupon->expiry_date ), 'apply_before_tax' => $coupon->apply_before_tax(), 'enable_free_shipping' => $coupon->enable_free_shipping(), 'product_categories' => $coupon->product_categories, @@ -200,6 +204,7 @@ class WC_API_Coupons extends WC_API_Resource { return $id; // TODO: implement + return $this->get_coupon( $id ); } diff --git a/includes/api/class-wc-api-customers.php b/includes/api/class-wc-api-customers.php index 55cf0978dc2..61653e569d4 100644 --- a/includes/api/class-wc-api-customers.php +++ b/includes/api/class-wc-api-customers.php @@ -103,7 +103,7 @@ class WC_API_Customers extends WC_API_Resource { $customers[] = $this->get_customer( $user_id, $fields ); } - // TODO: add navigation/total count headers for pagination + // TODO: add navigation/total count headers for pagination return array( 'customers' => $customers ); } @@ -127,7 +127,7 @@ class WC_API_Customers extends WC_API_Resource { $customer = new WP_User( $id ); // get info about user's last order - $last_order = $wpdb->get_row( "SELECT id, post_date + $last_order = $wpdb->get_row( "SELECT id, post_date_gmt FROM $wpdb->posts AS posts LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id WHERE meta.meta_key = '_customer_user' @@ -138,15 +138,15 @@ class WC_API_Customers extends WC_API_Resource { $customer_data = array( 'id' => $customer->ID, - 'created_at' => $customer->user_registered, + 'created_at' => $this->server->format_datetime( $customer->user_registered ), 'email' => $customer->user_email, 'first_name' => $customer->first_name, 'last_name' => $customer->last_name, 'username' => $customer->user_login, 'last_order_id' => is_object( $last_order ) ? $last_order->id : null, - 'last_order_date' => is_object( $last_order ) ? $last_order->post_date : null, - 'orders_count' => $customer->_order_count, - 'total_spent' => (string) number_format( $customer->_money_spent, 2 ), + 'last_order_date' => is_object( $last_order ) ? $this->server->format_datetime( $last_order->post_date_gmt ) : null, + 'orders_count' => (int) $customer->_order_count, + 'total_spent' => woocommerce_format_decimal( $customer->_money_spent ), 'avatar_url' => $this->get_avatar_url( $customer->customer_email ), 'billing_address' => array( 'first_name' => $customer->billing_first_name, @@ -188,7 +188,8 @@ class WC_API_Customers extends WC_API_Resource { $query = $this->query_customers( $filter ); - // TODO: permissions? + if ( ! current_user_can( 'list_users' ) ) + return new WP_Error( 'woocommerce_api_user_cannot_read_customer', __( 'You do not have permission to read customers', 'woocommerce' ), array( 'status' => 401 ) ); return array( 'count' => $query->get_total() ); } @@ -311,10 +312,10 @@ class WC_API_Customers extends WC_API_Resource { $query_args['offset'] = $args['offset']; if ( ! empty( $args['created_at_min'] ) ) - $this->created_at_min = $args['created_at_min']; + $this->created_at_min = $this->server->parse_datetime( $args['created_at_min'] ); if ( ! empty( $args['created_at_max'] ) ) - $this->created_at_max = $args['created_at_max']; + $this->created_at_max = $this->server->parse_datetime( $args['created_at_max'] ); // TODO: support page argument - requires custom implementation as WP_User_Query has no built-in pagination like WP_Query @@ -352,10 +353,10 @@ class WC_API_Customers extends WC_API_Resource { public function modify_user_query( $query ) { if ( $this->created_at_min ) - $query->query_where .= sprintf( " AND DATE(user_registered) >= '%s'", date( 'Y-m-d H:i:s', strtotime( $this->created_at_min ) ) ); // TODO: date formatting + $query->query_where .= sprintf( " AND user_registered >= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_min ) ); if ( $this->created_at_max ) - $query->query_where .= sprintf( " AND DATE(user_registered) <= '%s'", date( 'Y-m-d H:i:s', strtotime( $this->created_at_max ) ) ); // TODO: date formatting + $query->query_where .= sprintf( " AND user_registered <= STR_TO_DATE( '%s', '%%Y-%%m-%%d %%h:%%i:%%s' )", esc_sql( $this->created_at_max ) ); } /** diff --git a/includes/api/class-wc-api-json-handler.php b/includes/api/class-wc-api-json-handler.php index 9bad7855bbd..c08ebd4789a 100644 --- a/includes/api/class-wc-api-json-handler.php +++ b/includes/api/class-wc-api-json-handler.php @@ -61,7 +61,7 @@ class WC_API_JSON_Handler implements WC_API_Handler { WC()->api->server->send_status( 400 ); - $data = array( array( 'code' => 'woocommerce_api_json_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) );; + $data = array( array( 'code' => 'woocommerce_api_json_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ); } return $_GET['_jsonp'] . '(' . json_encode( $data ) . ')'; diff --git a/includes/api/class-wc-api-orders.php b/includes/api/class-wc-api-orders.php index 1f836e9a8d1..67ae8aaac08 100644 --- a/includes/api/class-wc-api-orders.php +++ b/includes/api/class-wc-api-orders.php @@ -76,6 +76,9 @@ class WC_API_Orders extends WC_API_Resource { foreach( $query->posts as $order_id ) { + if ( ! $this->is_readable( $order_id ) ) + continue; + $orders[] = $this->get_order( $order_id, $fields ); } @@ -103,23 +106,25 @@ class WC_API_Orders extends WC_API_Resource { $order = new WC_Order( $id ); + $order_post = get_post( $id ); + $order_data = array( 'id' => $order->id, 'order_number' => $order->get_order_number(), - 'created_at' => $order->order_date, - 'updated_at' => $order->modified_date, - 'completed_at' => $order->completed_date, + 'created_at' => $this->server->format_datetime( $order_post->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $order_post->post_modified_gmt ), + 'completed_at' => $this->server->format_datetime( $order->completed_date, true ), 'status' => $order->status, 'currency' => $order->order_currency, - 'total' => (string) $order->get_total(), - 'total_line_items_quantity' => (string) $order->get_item_count(), - 'total_tax' => (string) $order->get_total_tax(), - 'total_shipping' => (string) $order->get_total_shipping(), - 'cart_tax' => (string) $order->get_cart_tax(), - 'shipping_tax' => (string) $order->get_shipping_tax(), - 'total_discount' => (string) $order->get_total_discount(), - 'cart_discount' => (string) $order->get_cart_discount(), - 'order_discount' => (string) $order->get_order_discount(), + 'total' => woocommerce_format_decimal( $order->get_total() ), + 'total_line_items_quantity' => $order->get_item_count(), + 'total_tax' => woocommerce_format_decimal( $order->get_total_tax() ), + 'total_shipping' => woocommerce_format_decimal( $order->get_total_shipping() ), + 'cart_tax' => woocommerce_format_decimal( $order->get_cart_tax() ), + 'shipping_tax' => woocommerce_format_decimal( $order->get_shipping_tax() ), + 'total_discount' => woocommerce_format_decimal( $order->get_total_discount() ), + 'cart_discount' => woocommerce_format_decimal( $order->get_cart_discount() ), + 'order_discount' => woocommerce_format_decimal( $order->get_order_discount() ), 'shipping_methods' => $order->get_shipping_method(), 'payment_details' => array( 'method_id' => $order->payment_method, @@ -169,10 +174,10 @@ class WC_API_Orders extends WC_API_Resource { $order_data['line_items'][] = array( 'id' => $item_id, - 'subtotal' => (string) $order->get_line_subtotal( $item ), - 'total' => (string) $order->get_line_total( $item ), - 'total_tax' => (string) $order->get_line_tax( $item ), - 'quantity' => (string) $item['qty'], + 'subtotal' => woocommerce_format_decimal( $order->get_line_subtotal( $item ) ), + 'total' => woocommerce_format_decimal( $order->get_line_total( $item ) ), + 'total_tax' => woocommerce_format_decimal( $order->get_line_tax( $item ) ), + 'quantity' => (int) $item['qty'], 'tax_class' => ( ! empty( $item['tax_class'] ) ) ? $item['tax_class'] : null, 'name' => $item['name'], 'product_id' => ( isset( $product->variation_id ) ) ? $product->variation_id : $product->id, @@ -187,7 +192,7 @@ class WC_API_Orders extends WC_API_Resource { 'id' => $shipping_item_id, 'method_id' => $shipping_item['method_id'], 'method_title' => $shipping_item['name'], - 'total' => (string) number_format( $shipping_item['cost'], 2 ) + 'total' => woocommerce_format_decimal( $shipping_item['cost'] ), ); } @@ -197,7 +202,7 @@ class WC_API_Orders extends WC_API_Resource { $order_data['tax_lines'][] = array( 'code' => $tax_code, 'title' => $tax->label, - 'total' => (string) $tax->amount, + 'total' => woocommerce_format_decimal( $tax->amount ), 'compound' => (bool) $tax->is_compound, ); } @@ -209,8 +214,8 @@ class WC_API_Orders extends WC_API_Resource { 'id' => $fee_item_id, 'title' => $fee_item['name'], 'tax_class' => ( ! empty( $fee_item['tax_class'] ) ) ? $fee_item['tax_class'] : null, - 'total' => (string) $order->get_line_total( $fee_item ), - 'total_tax' => (string) $order->get_line_tax( $fee_item ), + 'total' => woocommerce_format_decimal( $order->get_line_total( $fee_item ) ), + 'total_tax' => woocommerce_format_decimal( $order->get_line_tax( $fee_item ) ), ); } @@ -220,7 +225,7 @@ class WC_API_Orders extends WC_API_Resource { $order_data['coupon_lines'] = array( 'id' => $coupon_item_id, 'code' => $coupon_item['name'], - 'amount' => (string) number_format( $coupon_item['discount_amount'], 2), + 'amount' => woocommerce_format_decimal( $coupon_item['discount_amount'] ), ); } @@ -263,7 +268,7 @@ class WC_API_Orders extends WC_API_Resource { if ( is_wp_error( $id ) ) return $id; - // TODO: implement + // TODO: implement, especially for status change return $this->get_order( $id ); } @@ -317,7 +322,7 @@ class WC_API_Orders extends WC_API_Resource { $order_notes[] = array( 'id' => $note->comment_ID, - 'created_at' => $note->comment_date_gmt, // TODO: date formatting + 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), 'note' => $note->comment_content, 'customer_note' => get_comment_meta( $note->comment_ID, 'is_customer_note', true ) ? true : false, ); diff --git a/includes/api/class-wc-api-products.php b/includes/api/class-wc-api-products.php index 49d8734a569..68b2d563b84 100644 --- a/includes/api/class-wc-api-products.php +++ b/includes/api/class-wc-api-products.php @@ -208,7 +208,7 @@ class WC_API_Products extends WC_API_Resource { $reviews[] = array( 'id' => $comment->comment_ID, - 'created_at' => $comment->comment_date_gmt, // TODO: date formatting + 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ), 'review' => $comment->comment_content, 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), 'reviewer_name' => $comment->comment_author, @@ -240,20 +240,19 @@ class WC_API_Products extends WC_API_Resource { if ( ! empty( $args['type'] ) ) { + $types = explode( ',', $args['type'] ); + $query_args['tax_query'] = array( array( 'taxonomy' => 'product_type', 'field' => 'slug', - 'terms' => $args['type'], + 'terms' => $types, ), ); unset( $args['type'] ); } - // TODO: some param to show hidden products, but hide by default - $query_args['meta_query'][] = WC()->query->visibility_meta_query(); - $query_args = $this->merge_query_args( $query_args, $args ); return new WP_Query( $query_args ); @@ -271,23 +270,23 @@ class WC_API_Products extends WC_API_Resource { return array( 'title' => $product->get_title(), 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, - 'created_at' => $product->get_post_data()->post_date_gmt, // TODO: date formatting - 'updated_at' => $product->get_post_data()->post_modified_gmt, // TODO: date formatting + 'created_at' => $this->server->format_datetime( $product->get_post_data()->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $product->get_post_data()->post_modified_gmt ), 'type' => $product->product_type, 'status' => $product->get_post_data()->post_status, 'downloadable' => $product->is_downloadable(), 'virtual' => $product->is_virtual(), 'permalink' => $product->get_permalink(), 'sku' => $product->get_sku(), - 'price' => (string) $product->get_price(), - 'regular_price' => (string) $product->get_regular_price(), - 'sale_price' => (string) $product->get_sale_price(), + 'price' => woocommerce_format_decimal( $product->get_price() ), + 'regular_price' => woocommerce_format_decimal( $product->get_regular_price() ), + 'sale_price' => $product->get_sale_price() ? woocommerce_format_decimal( $product->get_sale_price() ) : null, 'price_html' => $product->get_price_html(), 'taxable' => $product->is_taxable(), 'tax_status' => $product->get_tax_status(), 'tax_class' => $product->get_tax_class(), 'managing_stock' => $product->managing_stock(), - 'stock_quantity' => (string) $product->get_stock_quantity(), + 'stock_quantity' => (int) $product->get_stock_quantity(), 'in_stock' => $product->is_in_stock(), 'backorders_allowed' => $product->backorders_allowed(), 'backordered' => $product->is_on_backorder(), @@ -297,7 +296,7 @@ class WC_API_Products extends WC_API_Resource { 'visible' => $product->is_visible(), 'catalog_visibility' => $product->visibility, 'on_sale' => $product->is_on_sale(), - 'weight' => $product->get_weight(), + 'weight' => $product->get_weight() ? woocommerce_format_decimal( $product->get_weight() ) : null, 'dimensions' => array( 'length' => $product->length, 'width' => $product->width, @@ -311,18 +310,18 @@ class WC_API_Products extends WC_API_Resource { 'description' => apply_filters( 'the_content', $product->get_post_data()->post_content ), 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ), 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ), - 'average_rating' => $product->get_average_rating(), - 'rating_count' => $product->get_rating_count(), - 'related_ids' => array_values( $product->get_related() ), - 'upsell_ids' => $product->get_upsells(), - 'cross_sell_ids' => $product->get_cross_sells(), + 'average_rating' => woocommerce_format_decimal( $product->get_average_rating() ), + 'rating_count' => (int) $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( $product->get_related() ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsells() ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sells() ), 'categories' => wp_get_post_terms( $product->id, 'product_cat', array( 'fields' => 'names' ) ), 'tags' => wp_get_post_terms( $product->id, 'product_tag', array( 'fields' => 'names' ) ), 'images' => $this->get_images( $product ), 'attributes' => $this->get_attributes( $product ), 'downloads' => $this->get_downloads( $product ), - 'download_limit' => $product->download_limit, - 'download_expiry' => $product->download_expiry, + 'download_limit' => (int) $product->download_limit, + 'download_expiry' => (int) $product->download_expiry, 'download_type' => $product->download_type, 'purchase_note' => apply_filters( 'the_content', $product->purchase_note ), 'variations' => array(), @@ -350,25 +349,25 @@ class WC_API_Products extends WC_API_Resource { $variations[] = array( 'id' => $variation->get_variation_id(), - 'created_at' => $variation->get_post_data()->post_date_gmt, // TODO: date formatting - 'updated_at' => $variation->get_post_data()->post_modified_gmt, // TODO: date formatting + 'created_at' => $this->server->format_datetime( $variation->get_post_data()->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $variation->get_post_data()->post_modified_gmt ), 'downloadable' => $variation->is_downloadable(), 'virtual' => $variation->is_virtual(), 'permalink' => $variation->get_permalink(), 'sku' => $variation->get_sku(), - 'price' => (string) $variation->get_price(), - 'regular_price' => (string) $variation->get_regular_price(), - 'sale_price' => (string) $variation->get_sale_price(), + 'price' => woocommerce_format_decimal( $variation->get_price() ), + 'regular_price' => woocommerce_format_decimal( $variation->get_regular_price() ), + 'sale_price' => $variation->get_sale_price() ? woocommerce_format_decimal( $variation->get_sale_price() ) : null, 'taxable' => $variation->is_taxable(), 'tax_status' => $variation->get_tax_status(), 'tax_class' => $variation->get_tax_class(), - 'stock_quantity' => (string) $variation->get_stock_quantity(), + 'stock_quantity' => (int) $variation->get_stock_quantity(), 'in_stock' => $variation->is_in_stock(), 'backordered' => $variation->is_on_backorder(), 'purchaseable' => $variation->is_purchasable(), 'visible' => $variation->variation_is_visible(), 'on_sale' => $variation->is_on_sale(), - 'weight' => $variation->get_weight(), + 'weight' => $variation->get_weight() ? woocommerce_format_decimal( $variation->get_weight() ) : null, 'dimensions' => array( 'length' => $variation->length, 'width' => $variation->width, @@ -380,8 +379,8 @@ class WC_API_Products extends WC_API_Resource { 'image' => $this->get_images( $variation ), 'attributes' => $this->get_attributes( $variation ), 'downloads' => $this->get_downloads( $variation ), - 'download_limit' => $product->download_limit, - 'download_expiry' => $product->download_expiry, + 'download_limit' => (int) $product->download_limit, + 'download_expiry' => (int) $product->download_expiry, ); } @@ -438,7 +437,8 @@ class WC_API_Products extends WC_API_Resource { $images[] = array( 'id' => (int) $attachment_id, - 'created_at' => $attachment_post->post_date_gmt, // TODO: date formatting + 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ), 'src' => current( $attachment ), 'title' => get_the_title( $attachment_id ), 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), @@ -451,7 +451,8 @@ class WC_API_Products extends WC_API_Resource { $images[] = array( 'id' => 0, - 'created_at' => gmdate( 'Y-m-d H:i:s' ), // TODO: date formatting + 'created_at' => $this->server->format_datetime( time() ), // default to now + 'updated_at' => $this->server->format_datetime( time() ), 'src' => woocommerce_placeholder_img_src(), 'title' => __( 'Placeholder', 'woocommerce' ), 'alt' => __( 'Placeholder', 'woocommerce' ), diff --git a/includes/api/class-wc-api-reports.php b/includes/api/class-wc-api-reports.php index 6b42fa58f79..723883be168 100644 --- a/includes/api/class-wc-api-reports.php +++ b/includes/api/class-wc-api-reports.php @@ -156,6 +156,16 @@ class WC_API_Reports extends WC_API_Resource { 'function' => 'SUM', 'name' => 'total_shipping' ), + '_order_tax' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'total_tax' + ), + '_order_shipping_tax' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'total_shipping_tax' + ), 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', @@ -245,61 +255,61 @@ class WC_API_Reports extends WC_API_Resource { } $period_totals[ $time ] = array( - 'sales' => 0, + 'sales' => woocommerce_format_decimal( 0.00 ), 'orders' => 0, 'items' => 0, - 'tax' => 0, - 'shipping' => 0, - 'discount' => 0, + 'tax' => woocommerce_format_decimal( 0.00 ), + 'shipping' => woocommerce_format_decimal( 0.00 ), + 'discount' => woocommerce_format_decimal( 0.00 ), ); } // add total sales, total order count, total tax and total shipping for each period foreach ( $orders as $order ) { - $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date) ) : date( 'Y-m', strtotime( $order->post_date ) ); + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) continue; - $period_totals[ $time ]['sales'] = $order->total_sales; - $period_totals[ $time ]['orders'] = $order->total_orders; - $period_totals[ $time ]['tax'] = 1; - $period_totals[ $time ]['shipping'] = $order->total_shipping; + $period_totals[ $time ]['sales'] = woocommerce_format_decimal( $order->total_sales ); + $period_totals[ $time ]['orders'] = (int) $order->total_orders; + $period_totals[ $time ]['tax'] = woocommerce_format_decimal( $order->total_tax + $order->total_shipping_tax ); + $period_totals[ $time ]['shipping'] = woocommerce_format_decimal( $order->total_shipping ); } // add total order items for each period foreach ( $order_items as $order_item ) { - $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) continue; - $period_totals[ $time ]['items'] = $order_item->order_item_count; + $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; } // add total discount for each period foreach ( $discounts as $discount ) { - $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date) ) : date( 'Y-m', strtotime( $discount->post_date ) ); + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) continue; - $period_totals[ $time ]['discount'] = $discount->discount_amount; + $period_totals[ $time ]['discount'] = woocommerce_format_decimal( $discount->discount_amount ); } $sales_data = array( - 'sales' => $totals->sales, - 'average' => (string) number_format( $totals->sales / ( $this->report->chart_interval + 1 ), 2 ), - 'orders' => absint( $totals->order_count ), + 'sales' => woocommerce_format_decimal( $totals->sales ), + 'average' => woocommerce_format_decimal( $totals->sales / ( $this->report->chart_interval + 1 ) ), + 'orders' => (int) $totals->order_count, 'items' => $total_items, - 'tax' => (string) number_format( $totals->tax + $totals->shipping_tax, 2 ), - 'shipping' => $totals->shipping, - 'discount' => is_null( $total_discount ) ? 0 : $total_discount, - 'totals' => $period_totals, + 'tax' => woocommerce_format_decimal( $totals->tax + $totals->shipping_tax ), + 'shipping' => woocommerce_format_decimal( $totals->shipping ), + 'discount' => is_null( $total_discount ) ? woocommerce_format_decimal( 0.00 ) : woocommerce_format_decimal( $total_discount ), 'totals_grouped_by' => $this->report->chart_groupby, + 'totals' => $period_totals, ); return apply_filters( 'woocommerce_api_sales_report_response', array( 'sales' => $sales_data ), 'sales', $fields, $this->report, $this->server ); @@ -325,17 +335,28 @@ class WC_API_Reports extends WC_API_Resource { if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { // overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges - $_GET['start_date'] = $filter['date_min']; // TODO: date formatting? - $_GET['end_date'] = $filter['date_max']; // TODO: date formatting + $_GET['start_date'] = $this->server->parse_datetime( $filter['date_min'] ); + $_GET['end_date'] = isset( $filter['date_max'] ) ? $this->server->parse_datetime( $filter['date_max'] ) : null; } else { // default custom range to today $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); } - } - // TODO: handle invalid periods (e.g. `decade`) + } else { + + // ensure period is valid + if ( ! in_array( $filter['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) { + $filter['period'] = 'week'; + } + + // TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods + // allow "week" for period instead of "7day" + if ( 'week' === $filter['period'] ) { + $filter['period'] = '7day'; + } + } $this->report->calculate_current_range( $filter['period'] ); } diff --git a/includes/api/class-wc-api-resource.php b/includes/api/class-wc-api-resource.php index 1faee34d259..0b27c8db5cf 100644 --- a/includes/api/class-wc-api-resource.php +++ b/includes/api/class-wc-api-resource.php @@ -75,7 +75,7 @@ class WC_API_Resource { $post = get_post( $id, ARRAY_A ); - // TODO: redo this check, it's a bit janky + // for checking permissions, product variations are the same as the product post type $post_type = ( 'product_variation' === $post['post_type'] ) ? 'product' : $post['post_type']; // validate post type @@ -117,8 +117,6 @@ class WC_API_Resource { $args = array(); - // TODO: convert all dates from provided timezone into UTC - // TODO: return all dates in provided timezone, else UTC // date if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) { @@ -126,19 +124,19 @@ class WC_API_Resource { // resources created after specified date if ( ! empty( $request_args['created_at_min'] ) ) - $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $request_args['created_at_min'], 'inclusive' => true ); + $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true ); // resources created before specified date if ( ! empty( $request_args['created_at_max'] ) ) - $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $request_args['created_at_max'], 'inclusive' => true ); + $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true ); // resources updated after specified date if ( ! empty( $request_args['updated_at_min'] ) ) - $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $request_args['updated_at_min'], 'inclusive' => true ); + $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true ); // resources updated before specified date if ( ! empty( $request_args['updated_at_max'] ) ) - $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $request_args['updated_at_max'], 'inclusive' => true ); + $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true ); } // search diff --git a/includes/api/class-wc-api-server.php b/includes/api/class-wc-api-server.php index 772b2675fc4..0a4c5860537 100644 --- a/includes/api/class-wc-api-server.php +++ b/includes/api/class-wc-api-server.php @@ -571,71 +571,72 @@ class WC_API_Server { } /** - * Parse an RFC3339 timestamp into a DateTime + * Parse an RFC3339 datetime into a MySQl datetime * - * @param string $date RFC3339 timestamp - * @param boolean $force_utc Force UTC timezone instead of using the timestamp's TZ? - * @return DateTime + * Invalid dates default to unix epoch + * + * @since 2.1 + * @param string $datetime RFC3339 datetime + * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS) */ - public function parse_date( $date, $force_utc = false ) { - // Default timezone to the server's current one - $timezone = self::get_timezone(); - if ( $force_utc ) { - $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date ); + public function parse_datetime( $datetime ) { + + // Strip millisecond precision (a full stop followed by one or more digits) + if ( strpos( $datetime, '.' ) !== false ) { + $datetime = preg_replace( '/\.\d+/', '', $datetime ); + } + + // default timezone to UTC + $datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime ); + + try { + + $datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) ); + + } catch ( Exception $e ) { + + $datetime = new DateTime( '@0' ); + + } + + return $datetime->format( 'Y-m-d H:i:s' ); + } + + /** + * Format a unix timestamp or MySQL datetime into an RFC3339 datetime + * + * @since 2.1 + * @param int|string $timestamp unix timestamp or MySQL datetime + * @param bool $convert_to_utc + * @return string RFC3339 datetime + */ + public function format_datetime( $timestamp, $convert_to_utc = false ) { + + if ( $convert_to_utc ) { + $timezone = new DateTimeZone( woocommerce_timezone_string() ); + } else { $timezone = new DateTimeZone( 'UTC' ); } - // Strip millisecond precision (a full stop followed by one or more digits) - if ( strpos( $date, '.' ) !== false ) { - $date = preg_replace( '/\.\d+/', '', $date ); + try { + + if ( is_numeric( $timestamp ) ) { + $date = new DateTime( "@{$timestamp}" ); + } else { + $date = new DateTime( $timestamp, $timezone ); + } + + // convert to UTC by adjusting the time based on the offset of the site's timezone + if ( $convert_to_utc ) { + $date->modify( -1 * $date->getOffset() . ' seconds' ); + } + + } catch ( Exception $e ) { + + $date = new DateTime( '@0' ); } - $datetime = DateTime::createFromFormat( DateTime::RFC3339, $date ); // TODO: rewrite, PHP 5.3+ required for this - return $datetime; - } - - /** - * Get a local date with its GMT equivalent, in MySQL datetime format - * - * @param string $date RFC3339 timestamp - * @param boolean $force_utc Should we force UTC timestamp? - * @return array Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s) - */ - public function get_date_with_gmt( $date, $force_utc = false ) { - $datetime = $this->parse_date( $date, $force_utc ); - - $datetime->setTimezone( self::get_timezone() ); - $local = $datetime->format( 'Y-m-d H:i:s' ); - - $datetime->setTimezone( new DateTimeZone( 'UTC' ) ); - $utc = $datetime->format('Y-m-d H:i:s'); - - return array( $local, $utc ); - } - - /** - * Get the timezone object for the site - * - * @return DateTimeZone - */ - public function get_timezone() { - static $zone = null; - if ($zone !== null) - return $zone; - - $tzstring = get_option( 'timezone_string' ); - if ( ! $tzstring ) { - // Create a UTC+- zone if no timezone string exists - $current_offset = get_option( 'gmt_offset' ); - if ( 0 == $current_offset ) - $tzstring = 'UTC'; - elseif ($current_offset < 0) - $tzstring = 'Etc/GMT' . $current_offset; - else - $tzstring = 'Etc/GMT+' . $current_offset; - } - $zone = new DateTimeZone( $tzstring ); - return $zone; + return $date->format( 'Y-m-d\TH:i:s\Z' ); } /** diff --git a/includes/wc-formatting-functions.php b/includes/wc-formatting-functions.php index 617f2a029be..211d67df032 100644 --- a/includes/wc-formatting-functions.php +++ b/includes/wc-formatting-functions.php @@ -363,6 +363,51 @@ function woocommerce_time_format() { return apply_filters( 'woocommerce_time_format', get_option( 'time_format' ) ); } +/** + * WooCommerce Timezone - helper to retrieve the timezone string for a site until + * a WP core method exists (see http://core.trac.wordpress.org/ticket/24730) + * + * Adapted from http://www.php.net/manual/en/function.timezone-name-from-abbr.php#89155 + * + * @since 2.1 + * @access public + * @return string a valid PHP timezone string for the site + */ +function woocommerce_timezone_string() { + + // if site timezone string exists, return it + if ( $timezone = get_option( 'timezone_string' ) ) + return $timezone; + + // get UTC offset, if it isn't set then return UTC + if ( 0 === ( $utc_offset = get_option( 'gmt_offset', 0 ) ) ) + return 'UTC'; + + // adjust UTC offset from hours to seconds + $utc_offset *= 3600; + + // attempt to guess the timezone string from the UTC offset + $timezone = timezone_name_from_abbr( '', $utc_offset ); + + // last try, guess timezone string manually + if ( false === $timezone ) { + + $is_dst = date( 'I' ); + + foreach ( timezone_abbreviations_list() as $abbr ) { + foreach ( $abbr as $city ) { + + if ( $city['dst'] == $is_dst && $city['offset'] == $utc_offset ) { + return $city['timezone_id']; + } + } + } + } + + // fallback to UTC + return 'UTC'; +} + if ( ! function_exists( 'woocommerce_rgb_from_hex' ) ) { /** @@ -517,4 +562,4 @@ function wc_format_postcode( $postcode, $country ) { function wc_format_phone_number( $tel ) { $tel = str_replace( '.', '-', $tel ); return $tel; -} \ No newline at end of file +}