uawdijnntqw1x1x1
IP : 216.73.216.109
Hostname : premium160.web-hosting.com
Kernel : Linux premium160.web-hosting.com 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
Disable Function : None :)
OS : Linux
PATH:
/
home
/
batcwwjx
/
www
/
wp-content
/
plugins
/
woocommerce
/
.
/
src
/
Admin
/
.
/
API
/
Reports
/
Products
/
DataStore.php
/
/
<?php /** * API\Reports\Products\DataStore class file. */ namespace Automattic\WooCommerce\Admin\API\Reports\Products; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore; use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use Automattic\WooCommerce\Admin\API\Reports\TimeInterval; use Automattic\WooCommerce\Admin\API\Reports\SqlQuery; use Automattic\WooCommerce\Utilities\OrderUtil; use Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache; use Automattic\WooCommerce\Enums\ProductType; /** * API\Reports\Products\DataStore. */ class DataStore extends ReportsDataStore implements DataStoreInterface { /** * Table used to get the data. * * @override ReportsDataStore::$table_name * * @var string */ protected static $table_name = 'wc_order_product_lookup'; /** * Cache identifier. * * @override ReportsDataStore::$cache_key * * @var string */ protected $cache_key = 'products'; /** * Mapping columns to data type to return correct response types. * * @override ReportsDataStore::$column_types * * @var array */ protected $column_types = array( 'date_start' => 'strval', 'date_end' => 'strval', 'product_id' => 'intval', 'items_sold' => 'intval', 'net_revenue' => 'floatval', 'orders_count' => 'intval', // Extended attributes. 'name' => 'strval', 'price' => 'floatval', 'image' => 'strval', 'permalink' => 'strval', 'stock_status' => 'strval', 'stock_quantity' => 'intval', 'low_stock_amount' => 'intval', 'category_ids' => 'array_values', 'variations' => 'array_values', 'sku' => 'strval', ); /** * Extended product attributes to include in the data. * * @var array */ protected $extended_attributes = array( 'name', 'price', 'image', 'permalink', 'stock_status', 'stock_quantity', 'manage_stock', 'low_stock_amount', 'category_ids', 'variations', 'sku', ); /** * Data store context used to pass to filters. * * @override ReportsDataStore::$context * * @var string */ protected $context = 'products'; /** * Assign report columns once full table name has been assigned. * * @override ReportsDataStore::assign_report_columns() */ protected function assign_report_columns() { $table_name = self::get_db_table_name(); $this->report_columns = array( 'product_id' => 'product_id', 'items_sold' => 'SUM(product_qty) as items_sold', 'net_revenue' => 'SUM(product_net_revenue) AS net_revenue', 'orders_count' => "COUNT( DISTINCT ( CASE WHEN product_gross_revenue >= 0 THEN {$table_name}.order_id END ) ) as orders_count", ); } /** * Set up all the hooks for maintaining and populating table data. */ public static function init() { add_action( 'woocommerce_analytics_delete_order_stats', array( __CLASS__, 'sync_on_order_delete' ), 10 ); add_action( 'woocommerce_order_partially_refunded', array( __CLASS__, 'add_partial_refund_type_meta' ), 10, 2 ); add_action( 'woocommerce_order_fully_refunded', array( __CLASS__, 'add_full_refund_type_meta' ), 10, 2 ); } /** * Add a partial refund type meta to the order. * * @param int $order_id Order ID. * @param int $refund_id Refund ID. */ public static function add_partial_refund_type_meta( $order_id, $refund_id ) { self::add_refund_type_meta( $refund_id, 'partial' ); } /** * Add a full refund type meta to the order. * * @param int $order_id Order ID. * @param int $refund_id Refund ID. */ public static function add_full_refund_type_meta( $order_id, $refund_id ) { self::add_refund_type_meta( $refund_id, 'full' ); } /** * Add a refund type meta to the order. * * @param int $refund_id Refund ID. * @param string $type Refund type. */ public static function add_refund_type_meta( $refund_id, $type ) { $order = wc_get_order( $refund_id ); $order->update_meta_data( '_refund_type', $type ); $order->save_meta_data(); } /** * Fills FROM clause of SQL request based on user supplied parameters. * * @param array $query_args Parameters supplied by the user. * @param string $arg_name Target of the JOIN sql param. * @param string $id_cell ID cell identifier, like `table_name.id_column_name`. */ protected function add_from_sql_params( $query_args, $arg_name, $id_cell ) { global $wpdb; $type = 'join'; // Order by product name requires extra JOIN. switch ( $query_args['orderby'] ) { case 'product_name': $join = " JOIN {$wpdb->posts} AS _products ON {$id_cell} = _products.ID"; break; case 'sku': $join = " LEFT JOIN {$wpdb->postmeta} AS postmeta ON {$id_cell} = postmeta.post_id AND postmeta.meta_key = '_sku'"; break; case 'variations': $type = 'left_join'; $join = "LEFT JOIN ( SELECT post_parent, COUNT(*) AS variations FROM {$wpdb->posts} WHERE post_type = 'product_variation' GROUP BY post_parent ) AS _variations ON {$id_cell} = _variations.post_parent"; break; default: $join = ''; break; } if ( $join ) { if ( 'inner' === $arg_name ) { $this->subquery->add_sql_clause( $type, $join ); } else { $this->add_sql_clause( $type, $join ); } } } /** * Updates the database query with parameters used for Products report: categories and order status. * * @param array $query_args Query arguments supplied by the user. */ protected function add_sql_query_params( $query_args ) { global $wpdb; $order_product_lookup_table = self::get_db_table_name(); $this->add_time_period_sql_params( $query_args, $order_product_lookup_table ); $this->get_limit_sql_params( $query_args ); $this->add_order_by_sql_params( $query_args ); $included_products = $this->get_included_products( $query_args ); if ( $included_products ) { $this->add_from_sql_params( $query_args, 'outer', 'default_results.product_id' ); $this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.product_id IN ({$included_products})" ); } else { $this->add_from_sql_params( $query_args, 'inner', "{$order_product_lookup_table}.product_id" ); } $included_variations = $this->get_included_variations( $query_args ); if ( $included_variations ) { $this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id IN ({$included_variations})" ); } $order_status_filter = $this->get_status_subquery( $query_args ); if ( $order_status_filter ) { $this->subquery->add_sql_clause( 'join', "JOIN {$wpdb->prefix}wc_order_stats ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}wc_order_stats.order_id" ); $this->subquery->add_sql_clause( 'where', "AND ( {$order_status_filter} )" ); } } /** * Maps ordering specified by the user to columns in the database/fields in the data. * * @override ReportsDataStore::normalize_order_by() * * @param string $order_by Sorting criterion. * @return string */ protected function normalize_order_by( $order_by ) { if ( 'date' === $order_by ) { return self::get_db_table_name() . '.date_created'; } if ( 'product_name' === $order_by ) { return 'post_title'; } if ( 'sku' === $order_by ) { return 'meta_value'; } return $order_by; } /** * Enriches the product data with attributes specified by the extended_attributes. * * @param array $products_data Product data. * @param array $query_args Query parameters. */ protected function include_extended_info( &$products_data, $query_args ) { global $wpdb; $product_names = array(); foreach ( $products_data as $key => $product_data ) { $extended_info = new \ArrayObject(); if ( $query_args['extended_info'] ) { $product_id = $product_data['product_id']; $product = wc_get_product( $product_id ); // Product was deleted. if ( ! $product ) { if ( ! isset( $product_names[ $product_id ] ) ) { $product_names[ $product_id ] = $wpdb->get_var( $wpdb->prepare( "SELECT i.order_item_name FROM {$wpdb->prefix}wc_order_product_lookup l JOIN {$wpdb->prefix}woocommerce_order_items i ON i.order_item_id = l.order_item_id WHERE l.product_id = %d ORDER BY l.order_item_id DESC LIMIT 1", $product_id ) ); } /* translators: %s is product name */ $products_data[ $key ]['extended_info']['name'] = $product_names[ $product_id ] ? sprintf( __( '%s (Deleted)', 'woocommerce' ), $product_names[ $product_id ] ) : __( '(Deleted)', 'woocommerce' ); continue; } $extended_attributes = apply_filters( 'woocommerce_rest_reports_products_extended_attributes', $this->extended_attributes, $product_data ); foreach ( $extended_attributes as $extended_attribute ) { if ( 'variations' === $extended_attribute ) { if ( ! $product->is_type( ProductType::VARIABLE ) ) { continue; } $function = 'get_children'; } else { $function = 'get_' . $extended_attribute; } if ( is_callable( array( $product, $function ) ) ) { $value = $product->{$function}(); $extended_info[ $extended_attribute ] = $value; } } // If there is no set low_stock_amount, use the one in user settings. if ( '' === $extended_info['low_stock_amount'] ) { $extended_info['low_stock_amount'] = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); } $extended_info = $this->cast_numbers( $extended_info ); } $products_data[ $key ]['extended_info'] = $extended_info; } } /** * Returns the report data based on parameters supplied by the user. * * @override ReportsDataStore::get_data() * * @param array $query_args Query parameters. * @return stdClass|WP_Error Data. */ public function get_data( $query_args ) { $data = parent::get_data( $query_args ); /* * Do not cache extended info -- this is required to get the latest stock data. * `include_extended_info` checks only `extended_info` key, * so we don't need to bother about normalizing timestamps. */ $defaults = $this->get_default_query_vars(); $query_args = wp_parse_args( $query_args, $defaults ); $this->include_extended_info( $data->data, $query_args ); return $data; } /** * Get the default query arguments to be used by get_data(). * These defaults are only partially applied when used via REST API, as that has its own defaults. * * @override ReportsDataStore::get_default_query_vars() * * @return array Query parameters. */ public function get_default_query_vars() { $defaults = parent::get_default_query_vars(); $defaults['category_includes'] = array(); $defaults['product_includes'] = array(); $defaults['extended_info'] = false; return $defaults; } /** * Returns the report data based on normalized parameters. * Will be called by `get_data` if there is no data in cache. * * @override ReportsDataStore::get_noncached_data() * * @see get_data * @param array $query_args Query parameters. * @return stdClass|WP_Error Data object `{ totals: *, intervals: array, total: int, pages: int, page_no: int }`, or error. */ public function get_noncached_data( $query_args ) { global $wpdb; $table_name = self::get_db_table_name(); $this->initialize_queries(); $data = (object) array( 'data' => array(), 'total' => 0, 'pages' => 0, 'page_no' => 0, ); $selections = $this->selected_columns( $query_args ); $included_products = $this->get_included_products_array( $query_args ); $params = $this->get_limit_params( $query_args ); $this->add_sql_query_params( $query_args ); if ( count( $included_products ) > 0 ) { $filtered_products = array_diff( $included_products, array( '-1' ) ); $total_results = count( $filtered_products ); $total_pages = (int) ceil( $total_results / $params['per_page'] ); if ( 'date' === $query_args['orderby'] ) { $selections .= ", {$table_name}.date_created"; } $fields = $this->get_fields( $query_args ); $join_selections = $this->format_join_selections( $fields, array( 'product_id' ) ); $ids_table = $this->get_ids_table( $included_products, 'product_id' ); $this->subquery->clear_sql_clause( 'select' ); $this->subquery->add_sql_clause( 'select', $selections ); $this->add_sql_clause( 'select', $join_selections ); $this->add_sql_clause( 'from', '(' ); $this->add_sql_clause( 'from', $this->subquery->get_query_statement() ); $this->add_sql_clause( 'from', ") AS {$table_name}" ); $this->add_sql_clause( 'right_join', "RIGHT JOIN ( {$ids_table} ) AS default_results ON default_results.product_id = {$table_name}.product_id" ); $this->add_sql_clause( 'where', 'AND default_results.product_id != -1' ); $products_query = $this->get_query_statement(); } else { $count_query = "SELECT COUNT(*) FROM ( {$this->subquery->get_query_statement()} ) AS tt"; $db_records_count = (int) $wpdb->get_var( $count_query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared ); $total_results = $db_records_count; $total_pages = (int) ceil( $db_records_count / $params['per_page'] ); if ( ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) ) { return $data; } $this->subquery->clear_sql_clause( 'select' ); $this->subquery->add_sql_clause( 'select', $selections ); if ( in_array( $query_args['orderby'], array( 'items_sold', 'net_revenue', 'orders_count', 'variations' ), true ) ) { $this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) . ', product_id' ); } else { $this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) ); } $this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) ); $products_query = $this->subquery->get_query_statement(); } $product_data = $wpdb->get_results( $products_query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared ARRAY_A ); if ( null === $product_data ) { return $data; } $product_data = array_map( array( $this, 'cast_numbers' ), $product_data ); $data = (object) array( 'data' => $product_data, 'total' => $total_results, 'pages' => $total_pages, 'page_no' => (int) $query_args['page'], ); return $data; } /** * Create or update an entry in the wc_admin_order_product_lookup table for an order. * * @since 3.5.0 * @param int $order_id Order ID. * @return int|bool Returns -1 if order won't be processed, or a boolean indicating processing success. */ public static function sync_order_products( $order_id ) { global $wpdb; $order = wc_get_order( $order_id ); if ( ! $order ) { return -1; } $table_name = self::get_db_table_name(); $existing_items = $wpdb->get_col( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT order_item_id FROM {$table_name} WHERE order_id = %d", $order_id ) ); $existing_items = array_flip( $existing_items ); $order_items = $order->get_items(); $num_updated = 0; $decimals = wc_get_price_decimals(); $round_tax = 'no' === get_option( 'woocommerce_tax_round_at_subtotal' ); $is_full_refund_without_line_items = false; $partial_refund_product_revenue = array(); $refund_type = $order->get_meta( '_refund_type' ); $uses_new_full_refund_data = OrderUtil::uses_new_full_refund_data(); $parent_order = null; // When changing the order status to "Refunded", the refund order's type will be full refund, and the order items will be empty. // We need to get the parent order items, and exclude the items that are already being partially refunded. if ( 'shop_order_refund' === $order->get_type() && 'full' === $refund_type && empty( $order_items ) && $uses_new_full_refund_data ) { $is_full_refund_without_line_items = true; $parent_order_id = $order->get_parent_id(); $parent_order = wc_get_order( $parent_order_id ); $order_items = $parent_order->get_items(); // Get the partially refunded product and variation IDs along with their sum of product_net_revenue from the parent order. $partial_refund_products = $wpdb->get_results( $wpdb->prepare( " SELECT product_lookup.product_id, product_lookup.variation_id, SUM( product_lookup.product_net_revenue ) AS product_net_revenue FROM %i AS product_lookup INNER JOIN {$wpdb->prefix}wc_order_stats AS order_stats ON order_stats.order_id = product_lookup.order_id WHERE 1 = 1 AND order_stats.parent_id = %d AND product_lookup.product_net_revenue < 0 GROUP BY product_lookup.product_id, product_lookup.variation_id ", $table_name, $parent_order_id ) ); /** * Create a lookup table for partially refunded products. * E.g. [ * '1' => -20, * '2' => -40, * '51' => -10, * '52' => -30, * ] */ foreach ( $partial_refund_products as $product ) { $id = $product->variation_id ? $product->variation_id : $product->product_id; $partial_refund_product_revenue[ $id ] = (float) $product->product_net_revenue; } } foreach ( $order_items as $order_item ) { $order_item_id = $order_item->get_id(); unset( $existing_items[ $order_item_id ] ); $product_qty = $order_item->get_quantity( 'edit' ); $product_id = $order_item->get_product_id( 'edit' ); $variation_id = $order_item->get_variation_id( 'edit' ); $shipping_amount = $order->get_item_shipping_amount( $order_item ); $shipping_tax_amount = $order->get_item_shipping_tax_amount( $order_item ); $coupon_amount = $order->get_item_coupon_amount( $order_item ); $tax_amount = $order->get_item_cart_tax_amount( $order_item ); $net_revenue = round( $order_item->get_total( 'edit' ), $decimals ); // If the order is a full refund and there is no order items. The order item here is the parent order item. if ( $is_full_refund_without_line_items ) { $id = $variation_id ? $variation_id : $product_id; $partial_refund = $partial_refund_product_revenue[ $id ] ?? 0; // If a single line item was refunded 60% then fully refunded after, we need store the difference in the product lookup table. // E.g. A product costs $100, it was previously partially refunded $60, then fully refunded $40. // So it will be -abs( 100 + (-60) ) = -40. $net_revenue = -abs( $net_revenue + $partial_refund ); // Skip items that have already been fully refunded (single or multiple partial refunds). if ( 0.0 === $net_revenue ) { continue; } $product_qty = -abs( $product_qty ); // Set coupon amount to 0 for full refunds without line items. $coupon_amount = 0; if ( $parent_order ) { $remaining_refund_items = $parent_order->get_remaining_refund_items(); // Calculate the shipping amount to refund from the parent order. $total_shipping_refunded = $parent_order->get_total_shipping_refunded(); $shipping_total = (float) $parent_order->get_shipping_total(); $total_shipping_to_refund = $shipping_total - $total_shipping_refunded; if ( $total_shipping_to_refund > 0 ) { $shipping_amount = -abs( $parent_order->get_item_shipping_amount( $order_item, $remaining_refund_items, $total_shipping_to_refund ) ); } // Calculate the shipping tax amount to refund from the parent order. $shipping_tax = (float) $parent_order->get_shipping_tax(); $total_shipping_tax_refunded = $parent_order->get_total_shipping_tax_refunded(); $total_shipping_tax_to_refund = $shipping_tax - $total_shipping_tax_refunded; if ( $total_shipping_tax_to_refund > 0 ) { $shipping_tax_amount = -abs( $parent_order->get_item_shipping_tax_amount( $order_item, $remaining_refund_items, $total_shipping_tax_to_refund ) ); } // Calculate cart tax amount of the item from the parent order. $tax_amount = -abs( $parent_order->get_item_cart_tax_amount( $order_item ) ); } } $is_refund = $net_revenue < 0; // Skip line items without changes to product quantity. if ( ! $product_qty && ! $is_refund ) { ++$num_updated; continue; } if ( $round_tax ) { $tax_amount = round( $tax_amount, $decimals ); } $result = $wpdb->replace( self::get_db_table_name(), array( 'order_item_id' => $order_item_id, 'order_id' => $order->get_id(), 'product_id' => $product_id, 'variation_id' => $variation_id, 'customer_id' => $order->get_report_customer_id(), 'product_qty' => $product_qty, 'product_net_revenue' => $net_revenue, 'date_created' => $order->get_date_created( 'edit' )->date( TimeInterval::$sql_datetime_format ), 'coupon_amount' => $coupon_amount, 'tax_amount' => $tax_amount, 'shipping_amount' => $shipping_amount, 'shipping_tax_amount' => $shipping_tax_amount, // @todo Can this be incorrect if modified by filters? 'product_gross_revenue' => $net_revenue + $tax_amount + $shipping_amount + $shipping_tax_amount, ), array( '%d', // order_item_id. '%d', // order_id. '%d', // product_id. '%d', // variation_id. '%d', // customer_id. '%d', // product_qty. '%f', // product_net_revenue. '%s', // date_created. '%f', // coupon_amount. '%f', // tax_amount. '%f', // shipping_amount. '%f', // shipping_tax_amount. '%f', // product_gross_revenue. ) ); // WPCS: cache ok, DB call ok, unprepared SQL ok. /** * Fires when product's reports are updated. * * @param int $order_item_id Order Item ID. * @param int $order_id Order ID. */ do_action( 'woocommerce_analytics_update_product', $order_item_id, $order->get_id() ); // Sum the rows affected. Using REPLACE can affect 2 rows if the row already exists. $num_updated += 2 === intval( $result ) ? 1 : intval( $result ); } if ( ! empty( $existing_items ) ) { $existing_items = array_flip( $existing_items ); $format = array_fill( 0, count( $existing_items ), '%d' ); $format = implode( ',', $format ); array_unshift( $existing_items, $order_id ); $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared "DELETE FROM {$table_name} WHERE order_id = %d AND order_item_id in ({$format})", $existing_items ) ); } return ( count( $order_items ) === $num_updated ); } /** * Clean products data when an order is deleted. * * @param int $order_id Order ID. */ public static function sync_on_order_delete( $order_id ) { global $wpdb; $wpdb->delete( self::get_db_table_name(), array( 'order_id' => $order_id ) ); /** * Fires when product's reports are removed from database. * * @param int $product_id Product ID. * @param int $order_id Order ID. */ do_action( 'woocommerce_analytics_delete_product', 0, $order_id ); ReportsCache::invalidate(); } /** * Initialize query objects. */ protected function initialize_queries() { $this->clear_all_clauses(); $this->subquery = new SqlQuery( $this->context . '_subquery' ); $this->subquery->add_sql_clause( 'select', 'product_id' ); $this->subquery->add_sql_clause( 'from', self::get_db_table_name() ); $this->subquery->add_sql_clause( 'group_by', 'product_id' ); } }
/home/batcwwjx/www/wp-content/plugins/woocommerce/./src/Admin/./API/Reports/Products/DataStore.php