** * 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' ); 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' ); $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 ); // Skip line items without changes to product quantity. if ( ! $product_qty ) { $num_updated++; continue; } // Tax amount. $tax_amount = 0; $order_taxes = $order->get_taxes(); $tax_data = $order_item->get_taxes(); foreach ( $order_taxes as $tax_item ) { $tax_item_id = $tax_item->get_rate_id(); $tax_amount += isset( $tax_data['total'][ $tax_item_id ] ) ? (float) $tax_data['total'][ $tax_item_id ] : 0; } $net_revenue = round( $order_item->get_total( 'edit' ), $decimals ); 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' => wc_get_order_item_meta( $order_item_id, '_product_id' ), 'variation_id' => wc_get_order_item_meta( $order_item_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' ); } }